diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java new file mode 100644 index 00000000..be2ab9e6 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java @@ -0,0 +1,102 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.net.FeeRatesSource; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.Slider; +import javafx.util.StringConverter; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.sparrowwallet.sparrow.AppServices.*; + +public class FeeRangeSlider extends Slider { + public FeeRangeSlider() { + super(0, FEE_RATES_RANGE.size() - 1, 0); + setMajorTickUnit(1); + setMinorTickCount(0); + setSnapToTicks(false); + setShowTickLabels(true); + setShowTickMarks(true); + + setLabelFormatter(new StringConverter() { + @Override + public String toString(Double object) { + return Long.toString(FEE_RATES_RANGE.get(object.intValue())); + } + + @Override + public Double fromString(String string) { + return null; + } + }); + + updateTrackHighlight(); + } + + public double getFeeRate() { + return Math.pow(2.0, getValue()); + } + + public void setFeeRate(double feeRate) { + setValue(Math.log(feeRate) / Math.log(2)); + } + + public void updateTrackHighlight() { + addFeeRangeTrackHighlight(0); + } + + private void addFeeRangeTrackHighlight(int count) { + Platform.runLater(() -> { + Node track = lookup(".track"); + if(track != null) { + Map targetBlocksFeeRates = getTargetBlocksFeeRates(); + String highlight = ""; + if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) { + highlight += "#a0a1a766 " + getPercentageOfFeeRange(targetBlocksFeeRates.get(Integer.MAX_VALUE)) + "%, "; + } + highlight += "#41a9c966 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_TWO_HOURS - 1) + "%, "; + highlight += "#fba71b66 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HOUR - 1) + "%, "; + highlight += "#c8416466 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HALF_HOUR - 1) + "%"; + + track.setStyle("-fx-background-color: " + + "-fx-shadow-highlight-color, " + + "linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), " + + "linear-gradient(to bottom, derive(-fx-control-inner-background, -9%), derive(-fx-control-inner-background, 0%), derive(-fx-control-inner-background, -5%), derive(-fx-control-inner-background, -12%)), " + + "linear-gradient(to right, " + highlight + ")"); + } else if(count < 20) { + addFeeRangeTrackHighlight(count+1); + } + }); + } + + private Map getTargetBlocksFeeRates() { + Map retrievedFeeRates = AppServices.getTargetBlockFeeRates(); + if(retrievedFeeRates == null) { + retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> FALLBACK_FEE_RATE, + (u, v) -> { throw new IllegalStateException("Duplicate target blocks"); }, + LinkedHashMap::new)); + } + + return retrievedFeeRates; + } + + private int getPercentageOfFeeRange(Map targetBlocksFeeRates, Integer minTargetBlocks) { + List rates = new ArrayList<>(targetBlocksFeeRates.keySet()); + Collections.reverse(rates); + for(Integer targetBlocks : rates) { + if(targetBlocks < minTargetBlocks) { + return getPercentageOfFeeRange(targetBlocksFeeRates.get(targetBlocks)); + } + } + + return 100; + } + + private int getPercentageOfFeeRange(Double feeRate) { + double index = Math.log(feeRate) / Math.log(2); + return (int)Math.round(index * 10.0); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java index 519560bd..12d1c572 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.control; import com.google.common.base.Throwables; +import com.google.common.eventbus.Subscribe; import com.google.common.io.Files; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.address.Address; @@ -14,6 +15,9 @@ import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.UnitFormat; +import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.CardApi; import com.sparrowwallet.sparrow.io.Config; @@ -46,10 +50,7 @@ import tornadofx.control.Form; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR; @@ -62,6 +63,8 @@ public class PrivateKeySweepDialog extends Dialog { private final CopyableLabel keyAddress; private final ComboBoxTextField toAddress; private final ComboBox toWallet; + private final FeeRangeSlider feeRange; + private final CopyableLabel feeRate; public PrivateKeySweepDialog(Wallet wallet) { final DialogPane dialogPane = getDialogPane(); @@ -146,7 +149,24 @@ public class PrivateKeySweepDialog extends Dialog { stackPane.getChildren().addAll(toWallet, toAddress); toAddressField.getInputs().add(stackPane); - fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField); + Field feeRangeField = new Field(); + feeRangeField.setText("Fee range:"); + feeRange = new FeeRangeSlider(); + feeRange.setMaxWidth(320); + feeRangeField.getInputs().add(feeRange); + + Field feeRateField = new Field(); + feeRateField.setText("Fee rate:"); + feeRate = new CopyableLabel(); + feeRateField.getInputs().add(feeRate); + + feeRange.valueProperty().addListener((observable, oldValue, newValue) -> { + updateFeeRate(); + }); + feeRange.setFeeRate(AppServices.getDefaultFeeRate()); + updateFeeRate(); + + fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField, feeRangeField, feeRateField); form.getChildren().add(fieldset); dialogPane.setContent(form); @@ -201,6 +221,11 @@ public class PrivateKeySweepDialog extends Dialog { setResultConverter(dialogButton -> null); dialogPane.setPrefWidth(680); + EventManager.get().register(this); + setOnCloseRequest(event -> { + EventManager.get().unregister(this); + }); + ValidationSupport validationSupport = new ValidationSupport(); Platform.runLater(() -> { validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); @@ -357,7 +382,7 @@ public class PrivateKeySweepDialog extends Dialog { TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript()); noFeeTransaction.addOutput(sweepOutput); - Double feeRate = AppServices.getDefaultFeeRate(); + double feeRate = feeRange.getFeeRate(); long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate); if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) { fee++; @@ -425,6 +450,16 @@ public class PrivateKeySweepDialog extends Dialog { return glyph; } + private void updateFeeRate() { + UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat(); + feeRate.setText(format.getCurrencyFormat().format(feeRange.getFeeRate()) + " sats/vB"); + } + + @Subscribe + public void feeRatesUpdated(FeeRatesUpdatedEvent event) { + feeRange.updateTrackHighlight(); + } + public class PassphraseDialog extends Dialog { private final CustomPasswordField passphrase; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 86b1bc82..603869c6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -93,7 +93,7 @@ public class SendController extends WalletFormController implements Initializabl private Field feeRangeField; @FXML - private Slider feeRange; + private FeeRangeSlider feeRange; @FXML private CopyableLabel feeRate; @@ -306,21 +306,6 @@ public class SendController extends WalletFormController implements Initializabl feeRangeField.managedProperty().bind(feeRangeField.visibleProperty()); feeRangeField.visibleProperty().bind(targetBlocksField.visibleProperty().not()); - feeRange.setMin(0); - feeRange.setMax(FEE_RATES_RANGE.size() - 1); - feeRange.setMajorTickUnit(1); - feeRange.setMinorTickCount(0); - feeRange.setLabelFormatter(new StringConverter() { - @Override - public String toString(Double object) { - return Long.toString(FEE_RATES_RANGE.get(object.intValue())); - } - - @Override - public Double fromString(String string) { - return null; - } - }); feeRange.valueProperty().addListener(feeRangeListener); blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty()); @@ -424,8 +409,6 @@ public class SendController extends WalletFormController implements Initializabl } }); - addFeeRangeTrackHighlight(0); - efficiencyToggle.setOnAction(event -> { if(StandardAccount.WHIRLPOOL_MIX_ACCOUNTS.contains(getWalletForm().getWallet().getStandardAccountType()) && !overrideOptimizationStrategy) { AppServices.showWarningDialog("Privacy may be lost!", "It is recommended to optimize for privacy when sending coinjoined outputs."); @@ -846,12 +829,12 @@ public class SendController extends WalletFormController implements Initializabl } private Double getFeeRangeRate() { - return Math.pow(2.0, feeRange.getValue()); + return feeRange.getFeeRate(); } private void setFeeRangeRate(Double feeRate) { feeRange.valueProperty().removeListener(feeRangeListener); - feeRange.setValue(Math.log(feeRate) / Math.log(2)); + feeRange.setFeeRate(feeRate); feeRange.valueProperty().addListener(feeRangeListener); } @@ -981,47 +964,6 @@ public class SendController extends WalletFormController implements Initializabl } } - private void addFeeRangeTrackHighlight(int count) { - Platform.runLater(() -> { - Node track = feeRange.lookup(".track"); - if(track != null) { - Map targetBlocksFeeRates = getTargetBlocksFeeRates(); - String highlight = ""; - if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) { - highlight += "#a0a1a766 " + getPercentageOfFeeRange(targetBlocksFeeRates.get(Integer.MAX_VALUE)) + "%, "; - } - highlight += "#41a9c966 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_TWO_HOURS - 1) + "%, "; - highlight += "#fba71b66 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HOUR - 1) + "%, "; - highlight += "#c8416466 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HALF_HOUR - 1) + "%"; - - track.setStyle("-fx-background-color: " + - "-fx-shadow-highlight-color, " + - "linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), " + - "linear-gradient(to bottom, derive(-fx-control-inner-background, -9%), derive(-fx-control-inner-background, 0%), derive(-fx-control-inner-background, -5%), derive(-fx-control-inner-background, -12%)), " + - "linear-gradient(to right, " + highlight + ")"); - } else if(count < 20) { - addFeeRangeTrackHighlight(count+1); - } - }); - } - - private int getPercentageOfFeeRange(Map targetBlocksFeeRates, Integer minTargetBlocks) { - List rates = new ArrayList<>(targetBlocksFeeRates.keySet()); - Collections.reverse(rates); - for(Integer targetBlocks : rates) { - if(targetBlocks < minTargetBlocks) { - return getPercentageOfFeeRange(targetBlocksFeeRates.get(targetBlocks)); - } - } - - return 100; - } - - private int getPercentageOfFeeRange(Double feeRate) { - double index = Math.log(feeRate) / Math.log(2); - return (int)Math.round(index * 10.0); - } - private void updateMaxClearButtons(UtxoSelector utxoSelector, TxoFilter txoFilter) { if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector) { int num = presetUtxoSelector.getPresetUtxos().size(); @@ -1507,7 +1449,7 @@ public class SendController extends WalletFormController implements Initializabl } else { setFeeRatePriority(getFeeRangeRate()); } - addFeeRangeTrackHighlight(0); + feeRange.updateTrackHighlight(); } @Subscribe diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index 6fe8aca8..ed4f833d 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -25,6 +25,7 @@ +
@@ -88,7 +89,7 @@ - +