From 2058dbf084de78a820ab016343b2a5fb82b5d423 Mon Sep 17 00:00:00 2001 From: Thauan Amorim Date: Tue, 15 Jul 2025 19:12:50 -0300 Subject: [PATCH 1/4] 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(); From f72e660289e43c268e723a9ba0ed35445667773d Mon Sep 17 00:00:00 2001 From: Thauan Amorim Date: Tue, 15 Jul 2025 19:30:20 -0300 Subject: [PATCH 2/4] Warn Improvement --- .../sparrow/control/FeeRangeSlider.java | 18 +++++------------- .../sparrow/wallet/SendController.java | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java index 3db207fb..f636ce8c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java @@ -1,25 +1,17 @@ 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; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 22ec533a..71bbb97b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -456,8 +456,8 @@ public class SendController extends WalletFormController implements Initializabl validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.registerValidator(fee, Validator.combine( (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", userFeeSet.get() && insufficientInputsProperty.get()), - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() != null && getFeeValueSats() == 0), - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee Rate", isInsufficientFeeRate()) + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() == null || getFeeValueSats() == 0), + (Control c, String newValue) -> ValidationResult.fromWarningIf( c, "Insufficient Fee Rate", isInsufficientFeeRate()) )); validationSupport.setErrorDecorationEnabled(false); From c5076de9a5d3b64932f868ea8aa7e295d0aba021 Mon Sep 17 00:00:00 2001 From: Thauan Amorim Date: Tue, 15 Jul 2025 20:27:35 -0300 Subject: [PATCH 3/4] Fee rate simplification --- .../com/sparrowwallet/sparrow/AppServices.java | 4 ++-- .../sparrow/control/FeeRangeSlider.java | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 4b1e789a..956b1e24 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 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 List DOUBLE_FEE_RATES_RANGE = List.of(0.01D, 0.05D, 0.1D, 0.5, 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() - 8); 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 f636ce8c..b79af065 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java @@ -63,16 +63,14 @@ public class FeeRangeSlider extends Slider { public double getFeeRate() { 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); + // First range: 0.01, 0.05, 0.1 + if(value < 1) return 0.01 + (0.05 - 0.01) * value; + if(value < 2) return 0.05 + (0.1 - 0.05) * (value - 1); + // Second range: 0.1, 0.5, 1 + if(value < 3) return 0.1 + (0.5 - 0.1) * (value - 2); + if(value < 4) return 0.5 + (1.0 - 0.5) * (value - 3); + // Third range: 1, 2, 4, 8, ... + return Math.pow(2, value - 4 + 0) * 1.0; } public void setFeeRate(double feeRate) { From c86273b0cbeb1668f8e0da8c52fda763e3cc5106 Mon Sep 17 00:00:00 2001 From: Thauan Amorim Date: Tue, 15 Jul 2025 20:56:00 -0300 Subject: [PATCH 4/4] Fix: Create button is now disabled if the rate is invalid --- .../sparrowwallet/sparrow/wallet/SendController.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 71bbb97b..7c2e2626 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -178,6 +178,7 @@ public class SendController extends WalletFormController implements Initializabl setFiatFeeAmount(AppServices.getFiatCurrencyExchangeRate(), getFeeValueSats()); } + createButton.setDisable(isInsufficientFeeRate()); setTargetBlocks(getTargetBlocks()); updateTransaction(); } @@ -456,8 +457,8 @@ public class SendController extends WalletFormController implements Initializabl validationSupport.setValidationDecorator(new StyleClassValidationDecoration()); validationSupport.registerValidator(fee, Validator.combine( (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", userFeeSet.get() && insufficientInputsProperty.get()), - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", getFeeValueSats() == null || getFeeValueSats() == 0), - (Control c, String newValue) -> ValidationResult.fromWarningIf( c, "Insufficient Fee Rate", isInsufficientFeeRate()) + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Fee", isInsufficientFeeRate()), + (Control c, String newValue) -> ValidationResult.fromWarningIf( c, "Fee Rate Below Minimum", isBelowMinimumFeeRate()) )); validationSupport.setErrorDecorationEnabled(false); @@ -862,10 +863,14 @@ public class SendController extends WalletFormController implements Initializabl return AppServices.getMempoolHistogram(); } - public boolean isInsufficientFeeRate() { + public boolean isBelowMinimumFeeRate() { return walletTransactionProperty.get() != null && walletTransactionProperty.get().getFeeRate() < AppServices.getMinimumRelayFeeRate(); } + public boolean isInsufficientFeeRate() { + return getFeeValueSats() == null || getFeeValueSats() == 0; + } + private void setFeeRate(Double feeRateAmt) { UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat(); feeRate.setText(format.getCurrencyFormat().format(feeRateAmt) + (cpfpFeeRate.isVisible() ? "" : " sats/vB"));