add fee rate priority indicator

This commit is contained in:
Craig Raw 2021-03-08 15:25:03 +02:00
parent 13d701b0a7
commit 7ed75fc83d
5 changed files with 122 additions and 10 deletions

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.chart.Axis; import javafx.scene.chart.Axis;
@ -27,9 +28,11 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) { for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
Integer targetBlocks = targetBlocksIter.next(); Integer targetBlocks = targetBlocksIter.next();
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+"); if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks)); String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
feeRateSeries.getData().add(data); XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
}
} }
if(selectedTargetBlocks != null) { if(selectedTargetBlocks != null) {

View file

@ -40,6 +40,7 @@ public class FontAwesome5 extends GlyphFont {
LOCK_OPEN('\uf3c1'), LOCK_OPEN('\uf3c1'),
PEN_FANCY('\uf5ac'), PEN_FANCY('\uf5ac'),
PLUS('\uf067'), PLUS('\uf067'),
PLUS_CIRCLE('\uf055'),
QRCODE('\uf029'), QRCODE('\uf029'),
QUESTION_CIRCLE('\uf059'), QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'), RANDOM('\uf074'),

View file

@ -40,6 +40,9 @@ public enum FeeRatesSource {
}; };
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class); private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
public static final int BLOCKS_IN_HALF_HOUR = 3;
public static final int BLOCKS_IN_HOUR = 6;
public static final int BLOCKS_IN_TWO_HOURS = 12;
private final String name; private final String name;
@ -61,16 +64,20 @@ public enum FeeRatesSource {
Gson gson = new Gson(); Gson gson = new Gson();
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class); ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) { for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
if(blockTarget < 3) { if(blockTarget < BLOCKS_IN_HALF_HOUR) {
blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee); blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee);
} else if(blockTarget < 6) { } else if(blockTarget < BLOCKS_IN_HOUR) {
blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee); blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee);
} else if(blockTarget <= 10 || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) { } else if(blockTarget < BLOCKS_IN_TWO_HOURS || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) {
blockTargetFeeRates.put(blockTarget, threeTierRates.hourFee); blockTargetFeeRates.put(blockTarget, threeTierRates.hourFee);
} else { } else {
blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.get(blockTarget)); blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.get(blockTarget));
} }
} }
if(threeTierRates.minimumFee != null) {
blockTargetFeeRates.put(Integer.MAX_VALUE, threeTierRates.minimumFee);
}
} catch (Exception e) { } catch (Exception e) {
log.warn("Error retrieving recommended fee rates from " + url, e); log.warn("Error retrieving recommended fee rates from " + url, e);
} }
@ -98,5 +105,6 @@ public enum FeeRatesSource {
Double fastestFee; Double fastestFee;
Double halfHourFee; Double halfHourFee;
Double hourFee; Double hourFee;
Double minimumFee;
} }
} }

View file

@ -11,9 +11,11 @@ import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import com.sparrowwallet.sparrow.net.MempoolRateSize; import com.sparrowwallet.sparrow.net.MempoolRateSize;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.*; import javafx.beans.property.*;
@ -28,6 +30,7 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult; import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport; import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator; import org.controlsfx.validation.Validator;
@ -79,6 +82,12 @@ public class SendController extends WalletFormController implements Initializabl
@FXML @FXML
private CopyableLabel feeRate; private CopyableLabel feeRate;
@FXML
private Label feeRatePriority;
@FXML
private Glyph feeRatePriorityGlyph;
@FXML @FXML
private TextField fee; private TextField fee;
@ -350,6 +359,8 @@ public class SendController extends WalletFormController implements Initializabl
}); });
} }
}); });
addFeeRangeTrackHighlight(0);
} }
private void initializeTabHeader(int count) { private void initializeTabHeader(int count) {
@ -558,10 +569,12 @@ public class SendController extends WalletFormController implements Initializabl
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates(); Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
int maxTargetBlocks = 1; int maxTargetBlocks = 1;
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) { for(Integer targetBlocks : targetBlocksFeeRates.keySet()) {
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks); if(TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
Double candidate = targetBlocksFeeRates.get(targetBlocks); maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
if(Math.round(feeRate) >= Math.round(candidate)) { Double candidate = targetBlocksFeeRates.get(targetBlocks);
return targetBlocks; if(Math.round(feeRate) >= Math.round(candidate)) {
return targetBlocks;
}
} }
} }
@ -623,6 +636,45 @@ public class SendController extends WalletFormController implements Initializabl
private void setFeeRate(Double feeRateAmt) { private void setFeeRate(Double feeRateAmt) {
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte"); feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte");
setFeeRatePriority(feeRateAmt);
}
private void setFeeRatePriority(Double feeRateAmt) {
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
Integer targetBlocks = getTargetBlocks(feeRateAmt);
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE);
if(feeRateAmt <= minFeeRate) {
feeRatePriority.setText("Below Minimum");
feeRatePriorityGlyph.setStyle("-fx-text-fill: #a0a1a7cc");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
return;
}
Double lowestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(TARGET_BLOCKS_RANGE.size() - 1));
if(lowestBlocksRate > minFeeRate && feeRateAmt < (minFeeRate + ((lowestBlocksRate - minFeeRate) / 2))) {
feeRatePriority.setText("Try Then Replace");
feeRatePriorityGlyph.setStyle("-fx-text-fill: #7eb7c9cc");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.PLUS_CIRCLE);
return;
}
}
if(targetBlocks != null) {
if(targetBlocks < FeeRatesSource.BLOCKS_IN_HALF_HOUR) {
feeRatePriority.setText("High Priority");
feeRatePriorityGlyph.setStyle("-fx-text-fill: #c8416499");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
} else if(targetBlocks < FeeRatesSource.BLOCKS_IN_HOUR) {
feeRatePriority.setText("Medium Priority");
feeRatePriorityGlyph.setStyle("-fx-text-fill: #fba71b99");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
} else {
feeRatePriority.setText("Low Priority");
feeRatePriorityGlyph.setStyle("-fx-text-fill: #41a9c999");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
}
}
} }
private Node getSliderThumb() { private Node getSliderThumb() {
@ -635,6 +687,47 @@ 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, UtxoFilter utxoFilter) { private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) {
if(utxoSelector instanceof PresetUtxoSelector) { if(utxoSelector instanceof PresetUtxoSelector) {
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector; PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
@ -811,6 +904,7 @@ public class SendController extends WalletFormController implements Initializabl
if(targetBlocksField.isVisible()) { if(targetBlocksField.isVisible()) {
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks())); setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
} }
addFeeRangeTrackHighlight(0);
} }
@Subscribe @Subscribe

View file

@ -91,6 +91,12 @@
</Field> </Field>
<Field fx:id="feeRateField" text="Rate:"> <Field fx:id="feeRateField" text="Rate:">
<CopyableLabel fx:id="feeRate" /> <CopyableLabel fx:id="feeRate" />
<Region HBox.hgrow="ALWAYS" />
<Label fx:id="feeRatePriority" graphicTextGap="5">
<graphic>
<Glyph fx:id="feeRatePriorityGlyph" fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CIRCLE" />
</graphic>
</Label>
</Field> </Field>
<Field text="Fee:"> <Field text="Fee:">
<TextField fx:id="fee" styleClass="amount-field"/> <TextField fx:id="fee" styleClass="amount-field"/>