diff --git a/drongo b/drongo index 2ff9c94c..5d14be5c 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 2ff9c94c62d1f20e408f89d7a41e2e66426b8634 +Subproject commit 5d14be5c9c8cfaaa0fbf9d362b6609873dce38e0 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index e684ae08..812710d0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -4,9 +4,7 @@ import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.ReceiveActionEvent; -import com.sparrowwallet.sparrow.event.ReceiveToEvent; -import com.sparrowwallet.sparrow.event.ViewTransactionEvent; +import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.wallet.*; import javafx.application.Platform; @@ -14,11 +12,14 @@ import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; +import javafx.scene.layout.HBox; import org.controlsfx.glyphfont.FontAwesome; import org.controlsfx.glyphfont.Glyph; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.List; +import java.util.stream.Collectors; class EntryCell extends TreeTableCell { private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); @@ -84,6 +85,7 @@ class EntryCell extends TreeTableCell { tooltip.setText(hashIndexEntry.getHashIndex().toString()); setTooltip(tooltip); + HBox actionBox = new HBox(); Button viewTransactionButton = new Button(""); Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH); searchGlyph.setFontSize(12); @@ -91,7 +93,33 @@ class EntryCell extends TreeTableCell { viewTransactionButton.setOnAction(event -> { EventManager.get().post(new ViewTransactionEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry)); }); - setGraphic(viewTransactionButton); + actionBox.getChildren().add(viewTransactionButton); + + if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent()) { + Button spendUtxoButton = new Button(""); + Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND); + sendGlyph.setFontSize(12); + spendUtxoButton.setGraphic(sendGlyph); + spendUtxoButton.setOnAction(event -> { + List utxoEntries = getTreeTableView().getSelectionModel().getSelectedCells().stream() + .map(tp -> tp.getTreeItem().getValue()) + .filter(e -> e instanceof HashIndexEntry) + .map(e -> (HashIndexEntry)e) + .filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent()) + .collect(Collectors.toList()); + + if(!utxoEntries.contains(hashIndexEntry)) { + utxoEntries = List.of(hashIndexEntry); + } + + final List spendingUtxoEntries = utxoEntries; + EventManager.get().post(new SendActionEvent(utxoEntries)); + Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries))); + }); + actionBox.getChildren().add(spendUtxoButton); + } + + setGraphic(actionBox); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java new file mode 100644 index 00000000..092cce9d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/SendActionEvent.java @@ -0,0 +1,17 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.wallet.HashIndexEntry; + +import java.util.List; + +public class SendActionEvent { + private final List utxoEntries; + + public SendActionEvent(List utxoEntries) { + this.utxoEntries = utxoEntries; + } + + public List getUtxoEntries() { + return utxoEntries; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java new file mode 100644 index 00000000..26a406d9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/SpendUtxoEvent.java @@ -0,0 +1,17 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.wallet.HashIndexEntry; + +import java.util.List; + +public class SpendUtxoEvent { + private final List utxoEntries; + + public SpendUtxoEvent(List utxoEntries) { + this.utxoEntries = utxoEntries; + } + + public List getUtxoEntries() { + return utxoEntries; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/MaxUtxoSelector.java b/src/main/java/com/sparrowwallet/sparrow/wallet/MaxUtxoSelector.java new file mode 100644 index 00000000..a5ab84f9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/MaxUtxoSelector.java @@ -0,0 +1,14 @@ +package com.sparrowwallet.sparrow.wallet; + +import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; +import com.sparrowwallet.drongo.wallet.UtxoSelector; + +import java.util.Collection; +import java.util.Collections; + +public class MaxUtxoSelector implements UtxoSelector { + @Override + public Collection select(long targetValue, Collection candidates) { + return Collections.unmodifiableCollection(candidates); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 6b9feaae..38ccd0d1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.BitcoinUnit; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; +import com.sparrowwallet.sparrow.event.SpendUtxoEvent; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -50,6 +51,9 @@ public class SendController extends WalletFormController implements Initializabl @FXML private ComboBox amountUnit; + @FXML + private Button maxButton; + @FXML private Slider targetBlocks; @@ -69,10 +73,10 @@ public class SendController extends WalletFormController implements Initializabl private TransactionDiagram transactionDiagram; @FXML - private Button clear; + private Button clearButton; @FXML - private Button create; + private Button createButton; private final BooleanProperty userFeeSet = new SimpleBooleanProperty(false); @@ -129,6 +133,7 @@ public class SendController extends WalletFormController implements Initializabl addValidation(); address.textProperty().addListener((observable, oldValue, newValue) -> { + maxButton.setDisable(!isValidRecipientAddress()); updateTransaction(); }); @@ -145,6 +150,8 @@ public class SendController extends WalletFormController implements Initializabl } }); + maxButton.setDisable(!isValidRecipientAddress()); + insufficientInputsProperty.addListener((observable, oldValue, newValue) -> { amount.textProperty().removeListener(amountListener); String amt = amount.getText(); @@ -193,7 +200,7 @@ public class SendController extends WalletFormController implements Initializabl feeAmountUnit.valueProperty().addListener((observable, oldValue, newValue) -> { Long value = getFeeValueSats(oldValue); if(value != null) { - setFee(value); + setFeeValueSats(value); } }); @@ -210,20 +217,35 @@ 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"); + } + }); + walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> { if(walletTransaction != null) { + setRecipientValueSats(walletTransaction.getRecipientAmount()); + double feeRate = (double)walletTransaction.getFee() / walletTransaction.getTransaction().getVirtualSize(); if(userFeeSet.get()) { setTargetBlocks(getTargetBlocks(feeRate)); } else { - setFee(walletTransaction.getFee()); + setFeeValueSats(walletTransaction.getFee()); } setFeeRate(feeRate); } transactionDiagram.update(walletTransaction); - create.setDisable(walletTransaction == null); + createButton.setDisable(walletTransaction == null); }); address.setText("32YSPMaUePf511u5adEckiNq8QLec9ksXX"); @@ -247,12 +269,17 @@ public class SendController extends WalletFormController implements Initializabl } private void updateTransaction() { + updateTransaction(false); + } + + private void updateTransaction(boolean sendAll) { try { Address recipientAddress = getRecipientAddress(); - Long recipientAmount = getRecipientValueSats(); + Long recipientAmount = sendAll ? Long.valueOf(1L) : getRecipientValueSats(); if(recipientAmount != null && recipientAmount != 0 && (!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0))) { Wallet wallet = getWalletForm().getWallet(); - WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), recipientAddress, recipientAmount, getFeeRate(), userFeeSet.get() ? getFeeValueSats() : null); + Long userFee = userFeeSet.get() ? getFeeValueSats() : null; + WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), recipientAddress, recipientAmount, getFeeRate(), userFee, sendAll); walletTransactionProperty.setValue(walletTransaction); insufficientInputsProperty.set(false); @@ -268,6 +295,10 @@ public class SendController extends WalletFormController implements Initializabl } private List getUtxoSelectors() { + if(utxoSelectorProperty.get() != null) { + return List.of(utxoSelectorProperty.get()); + } + UtxoSelector priorityUtxoSelector = new PriorityUtxoSelector(AppController.getCurrentBlockHeight()); return List.of(priorityUtxoSelector); } @@ -299,6 +330,14 @@ public class SendController extends WalletFormController implements Initializabl return null; } + private void setRecipientValueSats(long recipientValue) { + amount.textProperty().removeListener(amountListener); + DecimalFormat df = new DecimalFormat("#.#"); + df.setMaximumFractionDigits(8); + amount.setText(df.format(amountUnit.getValue().getValue(recipientValue))); + amount.textProperty().addListener(amountListener); + } + private Long getFeeValueSats() { return getFeeValueSats(feeAmountUnit.getSelectionModel().getSelectedItem()); } @@ -312,6 +351,14 @@ public class SendController extends WalletFormController implements Initializabl return null; } + private void setFeeValueSats(long feeValue) { + fee.textProperty().removeListener(feeListener); + DecimalFormat df = new DecimalFormat("#.#"); + df.setMaximumFractionDigits(8); + fee.setText(df.format(feeAmountUnit.getValue().getValue(feeValue))); + fee.textProperty().addListener(feeListener); + } + private Integer getTargetBlocks() { int index = (int)targetBlocks.getValue(); return TARGET_BLOCKS_RANGE.get(index); @@ -356,14 +403,6 @@ public class SendController extends WalletFormController implements Initializabl feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte"); } - private void setFee(long feeValue) { - fee.textProperty().removeListener(feeListener); - DecimalFormat df = new DecimalFormat("#.#"); - df.setMaximumFractionDigits(8); - fee.setText(df.format(feeAmountUnit.getValue().getValue(feeValue))); - fee.textProperty().addListener(feeListener); - } - private Node getSliderThumb() { return targetBlocks.lookup(".thumb"); } @@ -373,14 +412,30 @@ public class SendController extends WalletFormController implements Initializabl } public void setMaxInput(ActionEvent event) { + UtxoSelector utxoSelector = utxoSelectorProperty.get(); + if(utxoSelector == null) { + MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector(); + utxoSelectorProperty.set(maxUtxoSelector); + } + updateTransaction(true); } public void clear(ActionEvent event) { address.setText(""); label.setText(""); + + amount.textProperty().removeListener(amountListener); amount.setText(""); + amount.textProperty().addListener(amountListener); + + fee.textProperty().removeListener(feeListener); fee.setText(""); + fee.textProperty().addListener(feeListener); + + userFeeSet.set(false); + targetBlocks.setValue(4); + utxoSelectorProperty.setValue(null); walletTransactionProperty.setValue(null); } @@ -394,4 +449,13 @@ public class SendController extends WalletFormController implements Initializabl feeRatesChart.select(getTargetBlocks()); setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks())); } + + @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()); + setUtxoSelector(new PresetUtxoSelector(utxos)); + updateTransaction(true); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index fd97b053..c32715f4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.ReceiveActionEvent; +import com.sparrowwallet.sparrow.event.SendActionEvent; import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent; import javafx.application.Platform; import javafx.fxml.FXML; @@ -113,4 +114,11 @@ public class WalletController extends WalletFormController implements Initializa selectFunction(Function.RECEIVE); } } + + @Subscribe + public void sendAction(SendActionEvent event) { + if(!event.getUtxoEntries().isEmpty() && event.getUtxoEntries().get(0).getWallet().equals(walletForm.getWallet())) { + selectFunction(Function.SEND); + } + } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index 62328c00..7239f9b0 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -99,9 +99,9 @@ -