From 2058dbf084de78a820ab016343b2a5fb82b5d423 Mon Sep 17 00:00:00 2001 From: Thauan Amorim Date: Tue, 15 Jul 2025 19:12:50 -0300 Subject: [PATCH] Support for fee rate below 1sat/vb --- .../sparrowwallet/sparrow/AppServices.java | 4 +- .../sparrow/control/FeeRangeSlider.java | 58 +++++++++++++------ .../transaction/HeadersController.java | 2 +- .../sparrow/wallet/SendController.java | 7 ++- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index ea719b55..4b1e789a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -87,8 +87,8 @@ public class AppServices { private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default"; public static final List TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50); - public static final List LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L); - public static final List FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3); + public static final List DOUBLE_FEE_RATES_RANGE = List.of(0.01D, 0.02D, 0.04D, 0.08D, 0.1D, 0.2D, 0.4D, 0.8D, 1D, 2D, 4D, 8D, 16D, 32D, 64D, 128D, 256D, 512D, 1024D, 2048D, 4096D, 8192D); + public static final List FEE_RATES_RANGE = DOUBLE_FEE_RATES_RANGE.subList(0, DOUBLE_FEE_RATES_RANGE.size() - 9); public static final double FALLBACK_FEE_RATE = 20000d / 1000; public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java index 335d9904..3db207fb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java @@ -1,17 +1,25 @@ package com.sparrowwallet.sparrow.control; +import static com.sparrowwallet.sparrow.AppServices.DOUBLE_FEE_RATES_RANGE; +import static com.sparrowwallet.sparrow.AppServices.FEE_RATES_RANGE; +import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE; +import static com.sparrowwallet.sparrow.AppServices.getFallbackFeeRate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + 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 { private static final double FEE_RATE_SCROLL_INCREMENT = 0.01; @@ -27,11 +35,11 @@ public class FeeRangeSlider extends Slider { setLabelFormatter(new StringConverter<>() { @Override public String toString(Double object) { - Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue()); - if(isLongFeeRange() && feeRate >= 1000) { + Double feeRate = DOUBLE_FEE_RATES_RANGE.get(object.intValue()); + if(isDoubleFeeRange() && feeRate >= 1000) { return feeRate / 1000 + "k"; } - return Long.toString(feeRate); + return feeRate < 1 ? Double.toString(feeRate) : String.format("%.0f", feeRate); } @Override @@ -51,10 +59,10 @@ public class FeeRangeSlider extends Slider { setOnScroll(event -> { if(event.getDeltaY() != 0) { double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT); - if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) { - newFeeRate = LONG_FEE_RATES_RANGE.get(0); - } else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) { - newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1); + if(newFeeRate < DOUBLE_FEE_RATES_RANGE.get(0)) { + newFeeRate = DOUBLE_FEE_RATES_RANGE.get(0); + } else if(newFeeRate > DOUBLE_FEE_RATES_RANGE.get(DOUBLE_FEE_RATES_RANGE.size() - 1)) { + newFeeRate = DOUBLE_FEE_RATES_RANGE.get(DOUBLE_FEE_RATES_RANGE.size() - 1); } setFeeRate(newFeeRate); } @@ -62,7 +70,17 @@ public class FeeRangeSlider extends Slider { } public double getFeeRate() { - return Math.pow(2.0, getValue()); + double value = getValue(); + // First range: 0.01, 0.02, 0.04, 0.08 and smooth values in between + if(value < 3) return 0.01 * Math.pow(2, value); + // Transition from 0.08 to 0.1 (smoothly, using factor 1.25) + if(value < 4) return 0.08 * Math.pow(1.25, value - 3); + // Second binary range: 0.1, 0.2, 0.4, 0.8 and smooth values in between + if(value < 7) return 0.1 * Math.pow(2, value - 4); + // Transition from 0.8 to 1.0 (smoothly, using factor 1.25) + if(value < 8) return 0.8 * Math.pow(1.25, value - 7); + // Third binary range: 1, 2, 4, 8, ... and smooth values in between + return Math.pow(2, value - 8); } public void setFeeRate(double feeRate) { @@ -72,16 +90,18 @@ public class FeeRangeSlider extends Slider { } private void updateMaxFeeRange(double value) { - if(value >= getMax() && !isLongFeeRange()) { - setMax(LONG_FEE_RATES_RANGE.size() - 1); + if(value >= getMax() && !isDoubleFeeRange()) { + setMin(FEE_RATES_RANGE.size() - 2); + setMax(DOUBLE_FEE_RATES_RANGE.size() - 1); updateTrackHighlight(); - } else if(value == getMin() && isLongFeeRange()) { + } else if(value == getMin() && isDoubleFeeRange()) { + setMin(0); setMax(FEE_RATES_RANGE.size() - 1); updateTrackHighlight(); } } - private boolean isLongFeeRange() { + private boolean isDoubleFeeRange() { return getMax() > FEE_RATES_RANGE.size() - 1; } @@ -138,8 +158,8 @@ public class FeeRangeSlider extends Slider { private int getPercentageOfFeeRange(Double feeRate) { double index = Math.log(feeRate) / Math.log(2); - if(isLongFeeRange()) { - index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99; + if(isDoubleFeeRange()) { + index *= ((double)FEE_RATES_RANGE.size() / (DOUBLE_FEE_RATES_RANGE.size())) * 0.99; } return (int)Math.round(index * 10.0); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 84bcee49..229994e2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -1139,7 +1139,7 @@ public class HeadersController extends TransactionFormController implements Init if(fee.getValue() > 0) { double feeRateAmt = fee.getValue() / headersForm.getTransaction().getVirtualSize(); - if(feeRateAmt > AppServices.LONG_FEE_RATES_RANGE.get(AppServices.LONG_FEE_RATES_RANGE.size() - 1)) { + if(feeRateAmt > AppServices.DOUBLE_FEE_RATES_RANGE.get(AppServices.DOUBLE_FEE_RATES_RANGE.size() - 1)) { Optional optType = AppServices.showWarningDialog("Very high fee rate!", "This transaction pays a very high fee rate of " + String.format("%.0f", feeRateAmt) + " sats/vB.\n\nBroadcast this transaction?", ButtonType.YES, ButtonType.NO); if(optType.isPresent() && optType.get() == ButtonType.NO) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 3cb46668..22ec533a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -900,13 +900,13 @@ public class SendController extends WalletFormController implements Initializabl } private void setFeeRatePriority(Double feeRateAmt) { + feeRateAmt = Math.round(feeRateAmt * 100.0) / 100.0; // Round to 2 decimal places Map targetBlocksFeeRates = getTargetBlocksFeeRates(); - Integer targetBlocks = getTargetBlocks(feeRateAmt); if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) { Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE); - if(minFeeRate > 1.0 && feeRateAmt < minFeeRate) { + if(feeRateAmt > 0.01 && feeRateAmt < minFeeRate) { feeRatePriority.setText("Below Minimum"); - feeRatePriority.setTooltip(new Tooltip("Transactions at this fee rate are currently being purged from the default sized mempool")); + feeRatePriority.setTooltip(new Tooltip("Transactions at this fee rate can be purged from the default sized mempool")); feeRatePriorityGlyph.setStyle("-fx-text-fill: #a0a1a7cc"); feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.EXCLAMATION_CIRCLE); return; @@ -922,6 +922,7 @@ public class SendController extends WalletFormController implements Initializabl } } + Integer targetBlocks = getTargetBlocks(feeRateAmt); if(targetBlocks != null) { if(targetBlocks < FeeRatesSource.BLOCKS_IN_HALF_HOUR) { Double maxFeeRate = FEE_RATES_RANGE.get(FEE_RATES_RANGE.size() - 1).doubleValue();