diff --git a/drongo b/drongo index b2297a8d..e8d8fa61 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit b2297a8d0219af70b3e97c83feb1af8aec485d9f +Subproject commit e8d8fa61268ec8ac4dd5c14e6715d4a4bde2fe49 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index b8ca9036..1893aa86 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -4,11 +4,15 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletTransaction; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.layout.*; @@ -146,6 +150,16 @@ public class TransactionDiagram extends GridPane { WalletNode walletNode = displayedUtxos.get(input); String desc = getInputDescription(input); Label label = new Label(desc); + label.getStyleClass().add("utxo-label"); + + Button excludeUtxoButton = new Button(""); + excludeUtxoButton.setGraphic(getExcludeGlyph()); + excludeUtxoButton.setOnAction(event -> { + EventManager.get().post(new ExcludeUtxoEvent(walletTx, input)); + }); + + label.setGraphic(excludeUtxoButton); + label.setContentDisplay(ContentDisplay.LEFT); Tooltip tooltip = new Tooltip(); if(walletNode != null) { @@ -324,6 +338,13 @@ public class TransactionDiagram extends GridPane { return spacer; } + public static Glyph getExcludeGlyph() { + Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE); + excludeGlyph.getStyleClass().add("exclude-utxo"); + excludeGlyph.setFontSize(12); + return excludeGlyph; + } + public static Glyph getPaymentGlyph() { Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND); paymentGlyph.getStyleClass().add("payment-icon"); @@ -332,35 +353,35 @@ public class TransactionDiagram extends GridPane { } public static Glyph getConsolidationGlyph() { - Glyph consolidationGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.REPLY_ALL); + Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL); consolidationGlyph.getStyleClass().add("consolidation-icon"); consolidationGlyph.setFontSize(12); return consolidationGlyph; } public static Glyph getChangeGlyph() { - Glyph changeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.COINS); + Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS); changeGlyph.getStyleClass().add("change-icon"); changeGlyph.setFontSize(12); return changeGlyph; } private Glyph getFeeGlyph() { - Glyph feeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.HAND_HOLDING); + Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING); feeGlyph.getStyleClass().add("fee-icon"); feeGlyph.setFontSize(12); return feeGlyph; } private Glyph getWarningGlyph() { - Glyph feeWarningGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.EXCLAMATION_CIRCLE); + Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); feeWarningGlyph.getStyleClass().add("fee-warning-icon"); feeWarningGlyph.setFontSize(12); return feeWarningGlyph; } private Glyph getLockGlyph() { - Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.LOCK); + Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK); lockGlyph.getStyleClass().add("lock-icon"); lockGlyph.setFontSize(12); return lockGlyph; diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ExcludeUtxoEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ExcludeUtxoEvent.java new file mode 100644 index 00000000..b66f1e83 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ExcludeUtxoEvent.java @@ -0,0 +1,22 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; +import com.sparrowwallet.drongo.wallet.WalletTransaction; + +public class ExcludeUtxoEvent { + private final WalletTransaction walletTransaction; + private final BlockTransactionHashIndex utxo; + + public ExcludeUtxoEvent(WalletTransaction walletTransaction, BlockTransactionHashIndex utxo) { + this.walletTransaction = walletTransaction; + this.utxo = utxo; + } + + public WalletTransaction getWalletTransaction() { + return walletTransaction; + } + + public BlockTransactionHashIndex getUtxo() { + return utxo; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 9b14c73a..64deb7f9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -40,6 +40,7 @@ public class FontAwesome5 extends GlyphFont { SATELLITE_DISH('\uf7c0'), SD_CARD('\uf7c2'), SEARCH('\uf002'), + TIMES_CIRCLE('\uf057'), TOOLS('\uf7d9'), UNDO('\uf0e2'), WALLET('\uf555'); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 339c0937..6693fd1c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -93,6 +93,8 @@ public class SendController extends WalletFormController implements Initializabl private final ObjectProperty utxoSelectorProperty = new SimpleObjectProperty<>(null); + private final ObjectProperty utxoFilterProperty = new SimpleObjectProperty<>(null); + private final ObjectProperty walletTransactionProperty = new SimpleObjectProperty<>(null); private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false); @@ -256,16 +258,11 @@ public class SendController extends WalletFormController implements Initializabl }); utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> { - if(utxoSelector instanceof PresetUtxoSelector) { - PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector; - int num = presetUtxoSelector.getPresetUtxos().size(); - String selection = " (" + num + " UTXO" + (num > 1 ? "s" : "") + " selected)"; - maxButton.setText("Max" + selection); - clearButton.setText("Clear" + selection); - } else { - maxButton.setText("Max"); - clearButton.setText("Clear"); - } + updateMaxClearButtons(utxoSelector, utxoFilterProperty.get()); + }); + + utxoFilterProperty.addListener((observable, oldValue, utxoFilter) -> { + updateMaxClearButtons(utxoSelectorProperty.get(), utxoFilter); }); walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> { @@ -328,7 +325,7 @@ 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(), recipientAddress, recipientAmount, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, sendAll, groupByAddress, includeMempoolChange); + WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), getUtxoFilters(), recipientAddress, recipientAmount, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, sendAll, groupByAddress, includeMempoolChange); walletTransactionProperty.setValue(walletTransaction); insufficientInputsProperty.set(false); @@ -355,6 +352,15 @@ public class SendController extends WalletFormController implements Initializabl return List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee)); } + private List getUtxoFilters() { + UtxoFilter utxoFilter = utxoFilterProperty.get(); + if(utxoFilter != null) { + return List.of(utxoFilter); + } + + return Collections.emptyList(); + } + private boolean isValidRecipientAddress() { try { getRecipientAddress(); @@ -471,10 +477,6 @@ public class SendController extends WalletFormController implements Initializabl return targetBlocks.lookup(".thumb"); } - public void setUtxoSelector(UtxoSelector utxoSelector) { - utxoSelectorProperty.set(utxoSelector); - } - public void setMaxInput(ActionEvent event) { UtxoSelector utxoSelector = utxoSelectorProperty.get(); if(utxoSelector == null) { @@ -509,6 +511,25 @@ public class SendController extends WalletFormController implements Initializabl return address.getScriptType().getDustThreshold(txOutput, getFeeRate()); } + private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) { + if(utxoSelector instanceof PresetUtxoSelector) { + PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector; + int num = presetUtxoSelector.getPresetUtxos().size(); + String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)"; + maxButton.setText("Max" + selection); + clearButton.setText("Clear" + selection); + } else if(utxoFilter instanceof ExcludeUtxoFilter) { + ExcludeUtxoFilter excludeUtxoFilter = (ExcludeUtxoFilter)utxoFilter; + int num = excludeUtxoFilter.getExcludedUtxos().size(); + String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)"; + maxButton.setText("Max" + exclusion); + clearButton.setText("Clear" + exclusion); + } else { + maxButton.setText("Max"); + clearButton.setText("Clear"); + } + } + public void scanQrAddress(ActionEvent event) { QRScanDialog qrScanDialog = new QRScanDialog(); Optional optionalResult = qrScanDialog.showAndWait(); @@ -547,6 +568,7 @@ public class SendController extends WalletFormController implements Initializabl userFeeSet.set(false); targetBlocks.setValue(4); utxoSelectorProperty.setValue(null); + utxoFilterProperty.setValue(null); walletTransactionProperty.setValue(null); validationSupport.setErrorDecorationEnabled(false); @@ -621,7 +643,8 @@ public class SendController extends WalletFormController implements Initializabl 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()); - setUtxoSelector(new PresetUtxoSelector(utxos)); + utxoSelectorProperty.set(new PresetUtxoSelector(utxos)); + utxoFilterProperty.set(null); updateTransaction(true); } } @@ -649,4 +672,37 @@ public class SendController extends WalletFormController implements Initializabl setFiatAmount(event.getCurrencyRate(), getRecipientValueSats()); setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats()); } + + @Subscribe + public void excludeUtxo(ExcludeUtxoEvent event) { + if(event.getWalletTransaction() == walletTransactionProperty.get()) { + UtxoSelector utxoSelector = utxoSelectorProperty.get(); + if(utxoSelector instanceof MaxUtxoSelector) { + Collection utxos = walletForm.getWallet().getWalletUtxos().keySet(); + utxos.remove(event.getUtxo()); + if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter) { + ExcludeUtxoFilter existingUtxoFilter = (ExcludeUtxoFilter)utxoFilterProperty.get(); + utxos.removeAll(existingUtxoFilter.getExcludedUtxos()); + } + PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(utxos); + utxoSelectorProperty.set(presetUtxoSelector); + updateTransaction(true); + } else if(utxoSelector instanceof PresetUtxoSelector) { + PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(((PresetUtxoSelector)utxoSelector).getPresetUtxos()); + presetUtxoSelector.getPresetUtxos().remove(event.getUtxo()); + utxoSelectorProperty.set(presetUtxoSelector); + updateTransaction(true); + } else { + ExcludeUtxoFilter utxoFilter = new ExcludeUtxoFilter(); + if(utxoFilterProperty.get() instanceof ExcludeUtxoFilter) { + ExcludeUtxoFilter existingUtxoFilter = (ExcludeUtxoFilter)utxoFilterProperty.get(); + utxoFilter.getExcludedUtxos().addAll(existingUtxoFilter.getExcludedUtxos()); + } + + utxoFilter.getExcludedUtxos().add(event.getUtxo()); + utxoFilterProperty.set(utxoFilter); + updateTransaction(); + } + } + } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css index b2999e97..52caa91c 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css @@ -65,4 +65,20 @@ -fx-text-fill: #696c77; -fx-stroke: #696c77; -fx-stroke-width: 1px; +} + +#transactionDiagram .utxo-label .button { + -fx-padding: 0; + -fx-pref-height: 18; + -fx-pref-width: 18; + -fx-border-width: 0; + -fx-background-color: -fx-background; +} + +#transactionDiagram .utxo-label .button .label .text { + -fx-fill: -fx-background; +} + +#transactionDiagram .utxo-label:hover .button .label .text { + -fx-fill: -fx-text-base-color; } \ No newline at end of file