add fee rate slider to private key sweep dialog

This commit is contained in:
Craig Raw 2023-11-07 14:02:17 +02:00
parent 910a400b18
commit 30408af719
4 changed files with 149 additions and 69 deletions

View file

@ -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<Double>() {
@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<Integer, Double> 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<Integer, Double> getTargetBlocksFeeRates() {
Map<Integer, Double> 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<Integer, Double> targetBlocksFeeRates, Integer minTargetBlocks) {
List<Integer> 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);
}
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address; 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.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; 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.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi; import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
@ -46,10 +50,7 @@ import tornadofx.control.Form;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.*;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR; import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
@ -62,6 +63,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private final CopyableLabel keyAddress; private final CopyableLabel keyAddress;
private final ComboBoxTextField toAddress; private final ComboBoxTextField toAddress;
private final ComboBox<Wallet> toWallet; private final ComboBox<Wallet> toWallet;
private final FeeRangeSlider feeRange;
private final CopyableLabel feeRate;
public PrivateKeySweepDialog(Wallet wallet) { public PrivateKeySweepDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
@ -146,7 +149,24 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
stackPane.getChildren().addAll(toWallet, toAddress); stackPane.getChildren().addAll(toWallet, toAddress);
toAddressField.getInputs().add(stackPane); 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); form.getChildren().add(fieldset);
dialogPane.setContent(form); dialogPane.setContent(form);
@ -201,6 +221,11 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
setResultConverter(dialogButton -> null); setResultConverter(dialogButton -> null);
dialogPane.setPrefWidth(680); dialogPane.setPrefWidth(680);
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
ValidationSupport validationSupport = new ValidationSupport(); ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> { Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
@ -357,7 +382,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript()); TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript());
noFeeTransaction.addOutput(sweepOutput); noFeeTransaction.addOutput(sweepOutput);
Double feeRate = AppServices.getDefaultFeeRate(); double feeRate = feeRange.getFeeRate();
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate); long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) { if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
fee++; fee++;
@ -425,6 +450,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
return glyph; 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<String> { public class PassphraseDialog extends Dialog<String> {
private final CustomPasswordField passphrase; private final CustomPasswordField passphrase;

View file

@ -93,7 +93,7 @@ public class SendController extends WalletFormController implements Initializabl
private Field feeRangeField; private Field feeRangeField;
@FXML @FXML
private Slider feeRange; private FeeRangeSlider feeRange;
@FXML @FXML
private CopyableLabel feeRate; private CopyableLabel feeRate;
@ -306,21 +306,6 @@ public class SendController extends WalletFormController implements Initializabl
feeRangeField.managedProperty().bind(feeRangeField.visibleProperty()); feeRangeField.managedProperty().bind(feeRangeField.visibleProperty());
feeRangeField.visibleProperty().bind(targetBlocksField.visibleProperty().not()); 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<Double>() {
@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); feeRange.valueProperty().addListener(feeRangeListener);
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty()); blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
@ -424,8 +409,6 @@ public class SendController extends WalletFormController implements Initializabl
} }
}); });
addFeeRangeTrackHighlight(0);
efficiencyToggle.setOnAction(event -> { efficiencyToggle.setOnAction(event -> {
if(StandardAccount.WHIRLPOOL_MIX_ACCOUNTS.contains(getWalletForm().getWallet().getStandardAccountType()) && !overrideOptimizationStrategy) { 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."); 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() { private Double getFeeRangeRate() {
return Math.pow(2.0, feeRange.getValue()); return feeRange.getFeeRate();
} }
private void setFeeRangeRate(Double feeRate) { private void setFeeRangeRate(Double feeRate) {
feeRange.valueProperty().removeListener(feeRangeListener); feeRange.valueProperty().removeListener(feeRangeListener);
feeRange.setValue(Math.log(feeRate) / Math.log(2)); feeRange.setFeeRate(feeRate);
feeRange.valueProperty().addListener(feeRangeListener); 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<Integer, Double> 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<Integer, Double> targetBlocksFeeRates, Integer minTargetBlocks) {
List<Integer> 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) { private void updateMaxClearButtons(UtxoSelector utxoSelector, TxoFilter txoFilter) {
if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector) { if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector) {
int num = presetUtxoSelector.getPresetUtxos().size(); int num = presetUtxoSelector.getPresetUtxos().size();
@ -1507,7 +1449,7 @@ public class SendController extends WalletFormController implements Initializabl
} else { } else {
setFeeRatePriority(getFeeRangeRate()); setFeeRatePriority(getFeeRangeRate());
} }
addFeeRangeTrackHighlight(0); feeRange.updateTrackHighlight();
} }
@Subscribe @Subscribe

View file

@ -25,6 +25,7 @@
<?import com.sparrowwallet.sparrow.wallet.FeeRatesSelection?> <?import com.sparrowwallet.sparrow.wallet.FeeRatesSelection?>
<?import com.sparrowwallet.sparrow.wallet.OptimizationStrategy?> <?import com.sparrowwallet.sparrow.wallet.OptimizationStrategy?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?> <?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import com.sparrowwallet.sparrow.control.FeeRangeSlider?>
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController"> <BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
<center> <center>
@ -88,7 +89,7 @@
<Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" /> <Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" />
</Field> </Field>
<Field fx:id="feeRangeField" text="Range:"> <Field fx:id="feeRangeField" text="Range:">
<Slider fx:id="feeRange" snapToTicks="false" showTickLabels="true" showTickMarks="true" /> <FeeRangeSlider fx:id="feeRange" />
</Field> </Field>
<Field fx:id="feeRateField" text="Rate:"> <Field fx:id="feeRateField" text="Rate:">
<CopyableLabel fx:id="feeRate" /> <CopyableLabel fx:id="feeRate" />