From 2ca8b91283e74f31e69dbaf96987078e0b5be42d Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 24 Nov 2020 20:34:51 +0200 Subject: [PATCH] add increase fee functionality for rbf transactions --- drongo | 2 +- .../sparrow/control/EntryCell.java | 105 ++++++++++++++++-- .../control/MempoolSizeFeeRatesChart.java | 4 +- .../sparrow/event/SendActionEvent.java | 19 +++- .../sparrow/event/SpendUtxoEvent.java | 42 ++++++- .../sparrow/glyphfont/FontAwesome5.java | 1 + .../sparrow/net/ElectrumServer.java | 15 ++- .../sparrow/net/SubscriptionService.java | 8 +- .../sparrow/wallet/PaymentController.java | 4 + .../sparrow/wallet/SendController.java | 83 ++++++++++---- .../sparrow/wallet/TransactionEntry.java | 4 +- .../sparrow/wallet/UtxosController.java | 6 +- .../sparrow/wallet/WalletController.java | 2 +- .../wallet/WalletTransactionsEntry.java | 3 +- src/main/resources/font/fa-solid-900.ttf | Bin 202616 -> 204528 bytes 15 files changed, 240 insertions(+), 58 deletions(-) diff --git a/drongo b/drongo index 49799fc0..6b20c655 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 49799fc0c8b5245a7931d0437a68172f9b6efbbc +Subproject commit 6b20c6558ab7cef6f582461692232a7687fe26c8 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 4ba67b3d..3efbaf2d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; -import com.sparrowwallet.drongo.wallet.BlockTransaction; -import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.protocol.NonStandardScriptException; +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.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -19,7 +22,7 @@ import org.controlsfx.glyphfont.Glyph; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; class EntryCell extends TreeTableCell { @@ -46,10 +49,10 @@ class EntryCell extends TreeTableCell { TransactionEntry transactionEntry = (TransactionEntry)entry; if(transactionEntry.getBlockTransaction().getHeight() == -1) { setText("Unconfirmed Parent"); - setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction())); + setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry)); } else if(transactionEntry.getBlockTransaction().getHeight() == 0) { setText("Unconfirmed"); - setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction())); + setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry)); } else { String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate()); setText(date); @@ -60,6 +63,7 @@ class EntryCell extends TreeTableCell { tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString()); setTooltip(tooltip); + HBox actionBox = new HBox(); Button viewTransactionButton = new Button(""); Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH); searchGlyph.setFontSize(12); @@ -67,7 +71,21 @@ class EntryCell extends TreeTableCell { viewTransactionButton.setOnAction(event -> { 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) { NodeEntry nodeEntry = (NodeEntry)entry; Address address = nodeEntry.getAddress(); @@ -139,9 +157,9 @@ class EntryCell extends TreeTableCell { utxoEntries = List.of(hashIndexEntry); } - final List spendingUtxoEntries = utxoEntries; - EventManager.get().post(new SendActionEvent(utxoEntries)); - Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries))); + final List spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList()); + EventManager.get().post(new SendActionEvent(hashIndexEntry.getWallet(), spendingUtxos)); + Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos))); }); actionBox.getChildren().add(spendUtxoButton); } @@ -151,8 +169,63 @@ class EntryCell extends TreeTableCell { } } + private static void increaseFee(TransactionEntry transactionEntry) { + BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); + Map walletTxos = transactionEntry.getWallet().getWalletTxos(); + List 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 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 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 externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs()); + externalOutputs.removeAll(ourOutputs); + List 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 { - public UnconfirmedTransactionContextMenu(BlockTransaction blockTransaction) { + public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) { + BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); MenuItem copyTxid = new MenuItem("Copy Transaction ID"); copyTxid.setOnAction(AE -> { hide(); @@ -161,7 +234,17 @@ class EntryCell extends TreeTableCell { 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); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java index 4e9ddd32..6eb03b70 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java @@ -174,7 +174,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { if(mvb >= 0.01) { Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB"); 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); getChildren().add(label); } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java index 092cce9d..486dc747 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java @@ -1,17 +1,24 @@ 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; public class SendActionEvent { - private final List utxoEntries; + private final Wallet wallet; + private final List utxos; - public SendActionEvent(List utxoEntries) { - this.utxoEntries = utxoEntries; + public SendActionEvent(Wallet wallet, List utxos) { + this.wallet = wallet; + this.utxos = utxos; } - public List getUtxoEntries() { - return utxoEntries; + public Wallet getWallet() { + return wallet; + } + + public List getUtxos() { + return utxos; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java index 26a406d9..5a3944ad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java @@ -1,17 +1,47 @@ 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; public class SpendUtxoEvent { - private final List utxoEntries; + private final Wallet wallet; + private final List utxos; + private final List payments; + private final Long fee; + private final boolean includeMempoolInputs; - public SpendUtxoEvent(List utxoEntries) { - this.utxoEntries = utxoEntries; + public SpendUtxoEvent(Wallet wallet, List utxos) { + this(wallet, utxos, null, null, false); } - public List getUtxoEntries() { - return utxoEntries; + public SpendUtxoEvent(Wallet wallet, List utxos, List payments, Long fee, boolean includeMempoolInputs) { + this.wallet = wallet; + this.utxos = utxos; + this.payments = payments; + this.fee = fee; + this.includeMempoolInputs = includeMempoolInputs; + } + + public Wallet getWallet() { + return wallet; + } + + public List getUtxos() { + return utxos; + } + + public List getPayments() { + return payments; + } + + public Long getFee() { + return fee; + } + + public boolean isIncludeMempoolInputs() { + return includeMempoolInputs; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 94b81c02..6e693dcf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -30,6 +30,7 @@ public class FontAwesome5 extends GlyphFont { ELLIPSIS_H('\uf141'), EYE('\uf06e'), HAND_HOLDING('\uf4bd'), + HAND_HOLDING_MEDICAL('\ue05c'), KEY('\uf084'), LAPTOP('\uf109'), LOCK('\uf023'), diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 1f91a015..081b4c91 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -138,7 +138,16 @@ public class ElectrumServer { public Map> getHistory(Wallet wallet, Collection nodes) throws ServerException { Map> nodeTransactionMap = new TreeMap<>(); - subscribeWalletNodes(wallet, nodes, nodeTransactionMap, 0); + + Set historyNodes = new HashSet<>(nodes); + //Add any nodes with mempool transactions in case these have been replaced + Set 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); Set newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet()); getReferencedTransactions(wallet, nodeTransactionMap); @@ -152,7 +161,7 @@ public class ElectrumServer { BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash()); for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) { WalletNode node = walletScriptHashes.get(getScriptHash(txOutput)); - if(node != null && !nodes.contains(node)) { + if(node != null && !historyNodes.contains(node)) { additionalNodes.add(node); } } @@ -162,7 +171,7 @@ public class ElectrumServer { if(inputBlockTransaction != null) { TransactionOutput txOutput = inputBlockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); WalletNode node = walletScriptHashes.get(getScriptHash(txOutput)); - if(node != null && !nodes.contains(node)) { + if(node != null && !historyNodes.contains(node)) { additionalNodes.add(node); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java index 1d90bce6..759ee3c2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.net; 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.JsonRpcService; import com.google.common.collect.Iterables; @@ -23,7 +24,12 @@ public class SubscriptionService { } @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 existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash); if(existingStatuses == null) { log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 08f3a9d1..8616f8c0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -255,6 +255,10 @@ public class PaymentController extends WalletFormController implements Initializ public void setPayment(Payment payment) { if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) { + address.setText(payment.getAddress().toString()); + if(payment.getLabel() != null) { + label.setText(payment.getLabel()); + } setRecipientValueSats(payment.getAmount()); setFiatAmount(AppController.getFiatCurrencyExchangeRate(), payment.getAmount()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 00b18e69..4bcb153e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -119,6 +119,8 @@ public class SendController extends WalletFormController implements Initializabl private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty(""); + private final BooleanProperty includeMempoolInputsProperty = new SimpleBooleanProperty(false); + private final ChangeListener feeListener = new ChangeListener<>() { @Override public void changed(ObservableValue observable, String oldValue, String newValue) { @@ -147,7 +149,7 @@ public class SendController extends WalletFormController implements Initializabl feeRate.setText("Unknown"); } - Tooltip tooltip = new Tooltip("Target confirmation within " + target + " blocks"); + Tooltip tooltip = new Tooltip("Target inclusion within " + target + " blocks"); targetBlocks.setTooltip(tooltip); userFeeSet.set(false); @@ -271,6 +273,7 @@ public class SendController extends WalletFormController implements Initializabl FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection(); feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection); + setDefaultFeeRate(); updateFeeRateSelection(feeRatesSelection); feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { @@ -318,15 +321,12 @@ public class SendController extends WalletFormController implements Initializabl walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> { if(walletTransaction != null) { - for(int i = 0; i < paymentTabs.getTabs().size(); i++) { - Payment payment = walletTransaction.getPayments().get(i); - PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData(); - controller.setPayment(payment); - } + setPayments(walletTransaction.getPayments()); double feeRate = walletTransaction.getFeeRate(); if(userFeeSet.get()) { setTargetBlocks(getTargetBlocks(feeRate)); + setFeeRangeRate(feeRate); } else { setFeeValueSats(walletTransaction.getFee()); } @@ -386,7 +386,8 @@ public class SendController extends WalletFormController implements Initializabl } 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 { FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml")); @@ -411,6 +412,22 @@ public class SendController extends WalletFormController implements Initializabl return payments; } + public void setPayments(List 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() { updateTransaction(null); } @@ -437,7 +454,8 @@ public class SendController extends WalletFormController implements Initializabl Integer currentBlockHeight = AppController.getCurrentBlockHeight(); boolean groupByAddress = Config.get().isGroupByAddress(); 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); insufficientInputsProperty.set(false); @@ -477,7 +495,11 @@ public class SendController extends WalletFormController implements Initializabl boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET); targetBlocksField.setVisible(blockTargetSelection); blockTargetFeeRatesChart.setVisible(blockTargetSelection); - setDefaultFeeRate(); + if(blockTargetSelection) { + setTargetBlocks(getTargetBlocks(getFeeRangeRate())); + } else { + setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks())); + } 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 index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget); Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget); - if(targetBlocksField.isVisible()) { - targetBlocks.setValue(index); - blockTargetFeeRatesChart.select(defaultTarget); - setFeeRate(defaultRate); - } else { - feeRange.setValue(Math.log(defaultRate) / Math.log(2)); - setFeeRate(getFeeRangeRate()); - } + targetBlocks.setValue(index); + blockTargetFeeRatesChart.select(defaultTarget); + setFeeRangeRate(defaultRate); + setFeeRate(getFeeRangeRate()); } private Long getFeeValueSats() { @@ -528,7 +546,7 @@ public class SendController extends WalletFormController implements Initializabl for(Integer targetBlocks : targetBlocksFeeRates.keySet()) { maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks); Double candidate = targetBlocksFeeRates.get(targetBlocks); - if(feeRate > candidate) { + if(Math.round(feeRate) >= Math.round(candidate)) { return targetBlocks; } } @@ -541,6 +559,8 @@ public class SendController extends WalletFormController implements Initializabl int index = TARGET_BLOCKS_RANGE.indexOf(target); targetBlocks.setValue(index); blockTargetFeeRatesChart.select(target); + Tooltip tooltip = new Tooltip("Target inclusion within " + target + " blocks"); + targetBlocks.setTooltip(tooltip); targetBlocks.valueProperty().addListener(targetBlocksListener); } @@ -559,6 +579,12 @@ public class SendController extends WalletFormController implements Initializabl 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() { if(targetBlocksField.isVisible()) { return getTargetBlocksFeeRates().get(getTargetBlocks()); @@ -631,9 +657,10 @@ public class SendController extends WalletFormController implements Initializabl fiatFeeAmount.setText(""); userFeeSet.set(false); - targetBlocks.setValue(4); + setDefaultFeeRate(); utxoSelectorProperty.setValue(null); utxoFilterProperty.setValue(null); + includeMempoolInputsProperty.set(false); walletTransactionProperty.setValue(null); createdWalletTransactionProperty.set(null); @@ -779,11 +806,23 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe public void spendUtxos(SpendUtxoEvent event) { - if(!event.getUtxoEntries().isEmpty() && event.getUtxoEntries().get(0).getWallet().equals(getWalletForm().getWallet())) { - List utxos = event.getUtxoEntries().stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList()); + if(!event.getUtxos().isEmpty() && event.getWallet().equals(getWalletForm().getWallet())) { + if(event.getPayments() != null) { + clear(null); + setPayments(event.getPayments()); + } + + if(event.getFee() != null) { + setFeeValueSats(event.getFee()); + userFeeSet.set(true); + } + + includeMempoolInputsProperty.set(event.isIncludeMempoolInputs()); + + List utxos = event.getUtxos(); utxoSelectorProperty.set(new PresetUtxoSelector(utxos)); utxoFilterProperty.set(null); - updateTransaction(true); + updateTransaction(event.getPayments() == null); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java index fdf43c12..ece8e2e5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java @@ -138,9 +138,7 @@ public class TransactionEntry extends Entry implements Comparable e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable()) .collect(Collectors.toList()); - EventManager.get().post(new SendActionEvent(utxoEntries)); - Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(utxoEntries))); + final List spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList()); + EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), spendingUtxos)); + Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), spendingUtxos))); } public void clear(ActionEvent event) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index 0bd97adb..bec99fd2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -121,7 +121,7 @@ public class WalletController extends WalletFormController implements Initializa @Subscribe 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); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java index c6f19af2..31019793 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java @@ -76,7 +76,8 @@ public class WalletTransactionsEntry extends Entry { entriesAdded.removeAll(entriesComplete); for(Entry entry : entriesAdded) { 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()); } } } diff --git a/src/main/resources/font/fa-solid-900.ttf b/src/main/resources/font/fa-solid-900.ttf index 5b979039ab28aaae305074541fe39258753ba624..e074608433ea79dea342cdbd7e0ab32dad0f7ed5 100644 GIT binary patch delta 17936 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%0APjLP8wq|(HtgE#Z{`xOc{``yzq*Gwv7_)dgPcH z7UJxulFiU01=voz+bKuJ9Jz*ULCRdOYCy~*PK1edXl=UL=`|A>YeH%mrxu??a!oHg zIcDBx#fO{9IK}j;8Hr%E$Cn%NdChR&kCDuk18wy{Yt=mGY?+wgcEOcqS5-Ssg%hlE zPKR9o1BTbxJvRwR;r96&%T0P3676+`%Z-TNrw!LjI40ZU(|D38D*PeEt1Af6D)!?V z3pa;NKWuE@AMyt$mcYe~XhVvN)=M&~D2C^9S!HYwg26gWv!rVB2dcWuW2s!q@ZFC{ z-DQaAAw2P09~L{}E+-86D>^IY5#9byXW5y-HY3ZE{!F02@}>P|zq7*k$Ndq6H~six zt-@f3q_HaL*f)?LOV+O-eo~LfP zEr+f1vIp$E5_WwSh7c!QI159_4#wY^-dg{_X;zi}S_wNR97nV}DvIGOSNJ9@&iBSL z%HsC4LG0}Ecv(iw>lX?leQ!8ghLzV$De#(cSrmUZh0jsC<5oJm;LZ!)40rAy943Q z>Xu&`ly%Qj5U1|#rR)`%J=c9{HG5NP&w)sVF{Fj{@bQ%#1VAy`_1G8`Dl#MsqFlxP zVwyubhL}a>Q!`I_aoUBtj~UZ8MV4rKSl2Jsa@i_mIR(t38OS%7Prq>5rtlI?i)i{P z9SvY(dx-vu)e=B&VbY+c5%f%At=RjAu)nC-G96;iv3t8;9m?(rhHHsn93VWd>2X&P zTo$`yDy!s1z}`8P#YYAVNqWb1y(-iSwMfMe4?o3zx+p<|oq$Sx%3X3+1Xd=9=GKO4 zoPCd--olbr0C7>SZ=8BTUr#hRB*UK8!m>hY=ti&N$;3HYTT73{qDgO-k!@ew!gB2U zTi7pPf9#rRtTrCTIZxAo>yP~rA}er)omOCbBFMCFoW}C9%qV989LN-ET*&Zi2%&SW z5FUVE5~HTQcN!~*1Xwo0TV7w->({)hl$Is1fT2>iFr9rYw(n)%v)u@o_$lW03dW)y z+plCHnhO^?Pj&U-NU{(i2ylS|r@DG)5)12;u)9&I3n0#)$wII$m37HaYbT`@g#4|e zLj_F>(yei0^53ej5aw1?*VJ4y`IO=!J$vkirrhdPH;p-M>@e@E6_c7uN(VE!@b%m? zhv%1#yD(lgHT%pF)7B?*UY9ejI^C43ZkkyH2fDI26dP1JZhCN7K{>nGKYBqe!_m{2 ztGgea%}&Z<=h=T*$xg+vPq~Pl%Ht*WgSh+LV-?*(e8`-rI>PB&#VURAOoYwd2*Xc- zflx34FC?X3!_{`{b!-GrChe~4*wpT?uVdeMg*qBxAGwuP*%=$yU>2|^ZeW9qkXqj3 zQLa=&ixf{!_ca@st!fR_?8v<17}$k(;us{`UV0}R!t?z0eRr}l_^I9d?_?jcaZo?* zjt?iwj`!fKi87pt;M|0rmzLJ|x}GkXvPb{$1H&VmQo8=78p;cLZb4F(o-Qw0f}|{D zb$@pkyM+53YR(=S+w6qX5xb*{h4a!_^>H$D2g={_Kgo1I)x};@qi$5eIau&e9+hB6 zT17+Q&5gJ3evm~V3Bw#YbmSY$OLV^k2veF<4^@-hUJYPhAj1S-7c&+SSS1oT3+`~ApDQUGlSCawmvic~2Tk^B@yrtwcB4~)*e~2`r+ame1D@m(dl9Z| z5`$Lv6MO0Z&Y!b{gW(i=Vb(7P@`L}%o_+IkEW7kSGf^|KcH#y9#zOa@=UB5KFetaY z7umU*07>oMupbBgLNgAaHXUJ&{v7yX<*aHz=Q$3_=SNu5pA}qnYF6+f#%4Kt5Px>S zPJYJ9weZ^TTGQJG4`FThlFwMbBt;<-rS{ohu~?0W4P1Swur?e?z*xHW*MD@`#0w=; zy71}|)=tSl%wQ>ym_8lhQ#9>8hEN(;Ry>9VHz&YLN#X(b-yXPu*b{F>d7B8@~ ze7uV11nlWPKA1)AD}6lBeVdPW2&|_&9OO3%=^@ycpO{F59jB*^J1&6zbcE**(n>T$ z*8}*&GABSj>Fi09&b}%w;q?VQ-Oz`B8OT5`MpTdTTO_Rliu^jdnGVsX_5)dbko{H` z|H$FtH`%-!Je-rm@AAVV8eq5%gE5LG+OasVvZuuP5U1#kabAa_&&5-HbKED^fr}UB zumTG3Xq@MXj=yb(a(R;^`(u9DJ}s9Ijrx3ds)iN{X}&JURVolm8cSr^L*0+%^6^IG zDkuXJOD`YsF%K4gMp9-NOw+>7%Sm9s=6j)AYOah!>t@6LuYqEv?~ zQItn>ubU<-Q`&QihGE-SUL2lfZ!G6U^_;U{iK3JQ8RVYx44$Y}d9kbv^+&i- zl1CK#xhg){vBY{cU*Jho;ro<2s@w4i5mS_4JsWvJ0w*+WeH1vi4PScf_q8I_*6OS9 zOxCo?o@ptYKRKk@Z#MGm?xT(T6PE2-(GK=J7Tz?BQp(A6g9ffi{#;X#CamK&yu5o;^M;J-tY;_tR;&E1IIm?F^GkF?sP(6VhocK0p*f&q( zbO2B@YitOHLJlk+%mk8@SVq9hWrt^%_{B#}R_o5M1>)6fJ z6a1d^>^ZsKVl%Ai>G^5lJT;P5Ss;3>qFGZ%RCS**okxV|Ni-EF9Ot7iH~>fmi}9Ta z_}MU%pTjck{+WC_FUquM%)*>vE;r8NGliHN2pTYT1JTU7>iS4N&d0^Mypa0~?6SE$ zH(cB#G@%iF$pnKU|zJsZb!X%vG+lhJjbRlT3p2vssuxdXsj}Pzu zb{;S0sS~UYyI=t?ig+wUFIn81=gnA>Y@TVU9#!#sRQt>Y{5x!g=C<;Gid>ZK*^Bv? zn)4Nnh2Z!KT5Y#1;S21?m+(bAZrJHd`RVCdR~MBP$Wqkn^@ni;Sw5<~eAIunwxWNx zEag{d?ax9z^|*ez6FI@3=fEL%mX@qwBk3sj`YQKZNmp&84m(3ZKH#AhrXtJOL8J@; zHedZ&aSLbulSeT= z3&$5$(AVxMR$X;fB{n=2sR&=0V9&UX=j98s>P7n*>?CZTozFqnE|i@e_@$4{MS>uT z0=GBa#C)(+*QCGLT^=n?Lan?Jk?A@~5 z91Y))Y~?W%2U}O4QZ@<+*2IX{uG`8B@!gWoZr#c+smy_cKapn9g>(h2ML@+lbbyDT zU{D77oUE*1wxoQ^vuI|aqi(MCUam5>TdJxST~>UR}96ceXjX@ zI6G1T;{&qY(ap!0NM7qC;$o(y+3$4oVW&Rl__7H|v}bP@0@>bp3dOEV8fM< zBgdqD|1Ms{C*|Awck!f^KPd-a-w0zpmY}bGNbgpD2X{okS%a-V$scEj>}Q_jhuER+ zyPx6{@Ck7i_8-si5eVSggP!GE-Gq0r1E>yW&BLGC>pqB5>UiHV=#GDpq-H1!81%&P z0m89`^X+-qp4h`n-E$9nO%Km!>Gr+&P*GAMvB)s{)gGS5Bl-4MJv@tt*W3Q*c(5JB zafD8Bc(BdbZ#X9hj-gJ?b4np7)tGQ`;IL5~__CJmTs>n(V_#R}49H=$E!x-Xs%F;e zDClz5jMNzyLl{d73Q8L@+{7-oL@FNVc+4I6|AW{MKF2>-GlS2{N;F@z?|hzTDxn;) zm}N)0pLw2N%(}Dw0COhQVmDc7=X}CLX8e@%m!Gw0aBW#7eq`VLN1krq^C#ZqsfCiL zbiDjjTK}JT%;(m_5g1ihSC-XQRocaed4_`(ZkcJ`-yv?NP^Z2P*;cv8fds%!0Sc;%#azxo*;#e%|EVVp2tm>^6PCJB>;DZCv^TId;gxai#O z--iU531^*mj{SaE7%6KPtyp~F3Og7P;%VZl#Vc0o&Sh}h!j)$(TfCyX?n}PD5K9UF m7(@jI7f*;YIss$%O%T4YTOvX*_`&CW?;`Dl{}0Na#{3_s<{SI~ delta 16054 zcmajG31C#!**E^2bJv->%|6Lwl1wJaB!Q4UnFIm^2z!KpVXG)XS;C@5L_|dslvYHl z$WSh=XyevWMa70TxJOGBEh=q!tyNT1wA5mymbTK`46m}uX>rp>;#p27C7Cst}5dR=bBn|Jp^6Isl@7uMT_$O0{Do#0rW7D~Q>(Xt~ zBCGK)q;WJOdi&ljpy#~#mh$Z^>H65IDSX0OQ%!8Xc$&_xOYLUEdS~#6B|A-ccj{Mc zD)XdbJXo7fJI&Mo1Uwx?(=$o#!$>)E8YR)a4=wkgy-Is}zvtb}&9lrNf1J;kNC#-$eM zWL#*>G%hfv8&i#H zqu3}i5=NoXm2bq2JR@f08aYO`5j8T5kP$QjhTrfSmZ2MN{h#{Z^i%q$`p5cl{eAt{ z`fK{D`b+vj{YAZBe@1^=e@Z`~@7JHu_vw%6kLr8%J^ByzALtM159s&nJ^BtkrQfM< z(QnhgrQf9ApkJr2*VpN*^;P;xeTCl9r7zc)=!^A5`X%~B`T~8nK1-jePt=?BvHBRj zUa!?F^in-f&(*W_h#u5Ex~XfrODAnmJFERk`&|2@_L=snc0&6|drv#2y`#OYy`}v| z`=$1V_L_EB`-S$Z_OkYp_H*r^*01$xKi2kXk7;|gN3a$!-WH)G_uFnXNef6~I?Sp83FG$ecD{8<9^0kni6>)D=L302&5kz%#%} zqR?idunM4Z2AIh}#mr`+2=XEah_X6xTERf{5K%Uevx_JfdAa>Wu^IsNWBZ8mP$v)N zaUDQ;9Q5O-iSk>4&A>70qJl=qF^r%9BPc8d)&p-5CD1sr12{!ggz};@M8(K2L7fsX zR)X}fHsAo!@F;MQsB|@O7}8S&_7jx_fFnfZQ-Hk?aP>r$$gAuosv@FlPZu8Gss;^e z_5oiJ)uM8p4(tHXI0**p!A$)rq6Um&6yA+%0n{H|3Y;Vwvx{hKGq9VesTe@paUP(D zXnX~5kZ8hYqUI*x4AI2BM3dTquA@YgrvPAL3Mx(k15-B;O#{JcZxKyz1->L|0f7tl z5zSBmbUrf*>?Ue$1P%bFiDs=Qnhho{JWSNKpJ>iP0PW_Yqj@^812_bHMKm9M%}0LM zd{kTjq6_MQ4x(>(fUQIqA$`#{qJ_x2xD)`r>k*M<-qN{*46#(j9jZs|PL$nrcuK}%V(BZmn0Pky&w>|*i`?@Ja z*Jl6+h;Bgn4PO#%*o*bQ5d}ByA-YKgIsg#u0>SQLU>k6pXd^n?c#P;~xnjju}!GE=_{gdwgLNyzSRjJed~Ur%}u~tM7PZ++OifnM0ERVqOGk&-$vUzx}rpP zVruR@O|%Ui+y!FW!NhhnN}+M;4AG7$z<#3dtObq`?bLyzL^jg90sx49w;lMB=x)&4 z-AHs#DX@p=UNCbnXnlVxQ4et64x;-pyZ4_VdH`*@9&96e2#p>B;UBCg`XQM4AsX!2 zOZ1~=qK8`nF!o3b@F3A%F!LztJi3JFF%W<3IML%9Aj4JzUlIKn?Rrtxi~Rl15d8%0 zf4Y{aF9vKSI#2r;ZXmjm)RP?K925gTP6m=RCk^qUZM$^|une&_wj2 z4jd->8SrxgQ1((Iu#f2F4B!aSq18mMv=hCG`oD+)=3i>90ZH*C2Kj#E

$on7EdvhVtZz_OIz$v2N60j9^Q7LeM=$#ti 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 HrTqT@%COpq