diff --git a/drongo b/drongo index e1f2ce41..2a456dd6 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit e1f2ce41ad2ca8771c6e79eaa488b12295ac6b0b +Subproject commit 2a456dd6027937705be7f6153b5a0a0eea757377 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 53df02ac..dcf42a4b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -91,8 +91,7 @@ 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); + private static final List LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d); public static final double FALLBACK_FEE_RATE = 20000d / 1000; public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000; @@ -142,6 +141,8 @@ public class AppServices { private static Double minimumRelayFeeRate; + private static Double serverMinimumRelayFeeRate; + private static CurrencyRate fiatCurrencyExchangeRate; private static List devices; @@ -211,6 +212,7 @@ public class AppServices { preventSleepService = createPreventSleepService(); onlineProperty.addListener(onlineServicesListener); + minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config); if(config.getMode() == Mode.ONLINE) { if(config.requiresInternalTor()) { @@ -750,6 +752,26 @@ public class AppServices { return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE); } + public static List getLongFeeRatesRange() { + if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) { + return LONG_FEE_RATES_RANGE; + } else { + List longFeeRatesRange = new ArrayList<>(); + longFeeRatesRange.add(minimumRelayFeeRate); + longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE); + return longFeeRatesRange; + } + } + + public static List getFeeRatesRange() { + if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) { + return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3); + } else { + List longFeeRatesRange = getLongFeeRatesRange(); + return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4); + } + } + public static Double getNextBlockMedianFeeRate() { return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate; } @@ -788,10 +810,18 @@ public class AppServices { }); } + public static Double getConfiguredMinimumRelayFeeRate(Config config) { + return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null; + } + public static Double getMinimumRelayFeeRate() { return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate; } + public static Double getServerMinimumRelayFeeRate() { + return serverMinimumRelayFeeRate; + } + public static CurrencyRate getFiatCurrencyExchangeRate() { return fiatCurrencyExchangeRate; } @@ -1219,7 +1249,10 @@ public class AppServices { public void newConnection(ConnectionEvent event) { currentBlockHeight = event.getBlockHeight(); System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight)); - minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE); + if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) { + minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate(); + } + serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate(); latestBlockHeader = event.getBlockHeader(); Config.get().addRecentServer(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java index 335d9904..a9d0a180 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FeeRangeSlider.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.net.FeeRatesSource; import javafx.application.Platform; @@ -7,6 +8,7 @@ import javafx.scene.Node; import javafx.scene.control.Slider; import javafx.util.StringConverter; +import java.text.DecimalFormat; import java.util.*; import java.util.stream.Collectors; @@ -14,9 +16,11 @@ import static com.sparrowwallet.sparrow.AppServices.*; public class FeeRangeSlider extends Slider { private static final double FEE_RATE_SCROLL_INCREMENT = 0.01; + private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0"); + private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###"); public FeeRangeSlider() { - super(0, FEE_RATES_RANGE.size() - 1, 0); + super(0, AppServices.getFeeRatesRange().size() - 1, 0); setMajorTickUnit(1); setMinorTickCount(0); setSnapToTicks(false); @@ -27,11 +31,11 @@ public class FeeRangeSlider extends Slider { setLabelFormatter(new StringConverter<>() { @Override public String toString(Double object) { - Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue()); + Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue()); if(isLongFeeRange() && feeRate >= 1000) { - return feeRate / 1000 + "k"; + return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k"; } - return Long.toString(feeRate); + return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate); } @Override @@ -51,10 +55,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 < AppServices.getLongFeeRatesRange().getFirst()) { + newFeeRate = AppServices.getLongFeeRatesRange().getFirst(); + } else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) { + newFeeRate = AppServices.getLongFeeRatesRange().getLast(); } setFeeRate(newFeeRate); } @@ -62,27 +66,79 @@ public class FeeRangeSlider extends Slider { } public double getFeeRate() { - return Math.pow(2.0, getValue()); + return getFeeRate(AppServices.getMinimumRelayFeeRate()); + } + + public double getFeeRate(Double minRelayFeeRate) { + if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) { + return Math.pow(2.0, getValue()); + } + + if(getValue() < 1.0d) { + if(minRelayFeeRate == 0.0d) { + return getValue(); + } + return Math.pow(minRelayFeeRate, 1.0d - getValue()); + } + + return Math.pow(2.0, getValue() - 1.0d); } public void setFeeRate(double feeRate) { - double value = Math.log(feeRate) / Math.log(2); + setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate()); + } + + public void setFeeRate(double feeRate, Double minRelayFeeRate) { + double value = getValue(feeRate, minRelayFeeRate); updateMaxFeeRange(value); setValue(value); } + private double getValue(double feeRate, Double minRelayFeeRate) { + double value; + + if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) { + value = Math.log(feeRate) / Math.log(2); + } else { + if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) { + if(minRelayFeeRate == 0.0d) { + return feeRate; + } + value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate)); + } else { + value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d; + } + } + + return value; + } + + public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) { + if(minRelayFeeRate != null && previousMinRelayFeeRate != null) { + setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate); + } + setMinorTickCount(1); + setMinorTickCount(0); + } + private void updateMaxFeeRange(double value) { if(value >= getMax() && !isLongFeeRange()) { - setMax(LONG_FEE_RATES_RANGE.size() - 1); + if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) { + setMin(1.0d); + } + setMax(AppServices.getLongFeeRatesRange().size() - 1); updateTrackHighlight(); } else if(value == getMin() && isLongFeeRange()) { - setMax(FEE_RATES_RANGE.size() - 1); + if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) { + setMin(0.0d); + } + setMax(AppServices.getFeeRatesRange().size() - 1); updateTrackHighlight(); } } - private boolean isLongFeeRange() { - return getMax() > FEE_RATES_RANGE.size() - 1; + public boolean isLongFeeRange() { + return getMax() > AppServices.getFeeRatesRange().size() - 1; } public void updateTrackHighlight() { @@ -137,9 +193,9 @@ public class FeeRangeSlider extends Slider { } private int getPercentageOfFeeRange(Double feeRate) { - double index = Math.log(feeRate) / Math.log(2); + double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate()); if(isLongFeeRange()) { - index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99; + index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99; } 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 38451a12..fb4392d4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java @@ -398,14 +398,14 @@ public class PrivateKeySweepDialog extends Dialog { double feeRate = feeRange.getFeeRate(); long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate); - if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) { + if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) { fee++; } long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE); if(total - fee <= dustThreshold) { - feeRate = Transaction.DEFAULT_MIN_RELAY_FEE; - fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1; + feeRate = AppServices.getMinimumRelayFeeRate(); + fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0); if(total - fee <= dustThreshold) { AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats)."); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 623fa920..26b65f4c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -225,7 +225,6 @@ public class TransactionDiagram extends GridPane { GridPane.setConstraints(outputsPane, 5, 0); getChildren().clear(); - getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); List userPayments = getUserPayments(); if(!isFinal() && userPayments.size() > 1) { @@ -234,6 +233,8 @@ public class TransactionDiagram extends GridPane { getChildren().add(totalsPane); } + getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); + if(contextMenu == null) { contextMenu = new ContextMenu(); MenuItem menuItem = new MenuItem("Save as Image..."); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java index 616d1436..f5013b03 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ConnectionEvent.java @@ -1,6 +1,7 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.protocol.BlockHeader; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.net.MempoolRateSize; import java.util.List; @@ -13,6 +14,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent { private final int blockHeight; private final BlockHeader blockHeader; private final Double minimumRelayFeeRate; + private final Double previousMinimumRelayFeeRate; public ConnectionEvent(List serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map targetBlockFeeRates, Set mempoolRateSizes, Double minimumRelayFeeRate) { super(targetBlockFeeRates, mempoolRateSizes); @@ -21,6 +23,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent { this.blockHeight = blockHeight; this.blockHeader = blockHeader; this.minimumRelayFeeRate = minimumRelayFeeRate; + this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate(); } public List getServerVersion() { @@ -42,4 +45,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent { public Double getMinimumRelayFeeRate() { return minimumRelayFeeRate; } + + public Double getPreviousMinimumRelayFeeRate() { + return previousMinimumRelayFeeRate; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index e328db62..61c77301 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io; import com.google.gson.*; import com.sparrowwallet.drongo.BitcoinUnit; +import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Theme; @@ -83,6 +84,7 @@ public class Config { private int maxPageSize = DEFAULT_PAGE_SIZE; private boolean usePayNym; private boolean mempoolFullRbf; + private double minRelayFeeRate = Transaction.DEFAULT_MIN_RELAY_FEE; private Double appWidth; private Double appHeight; @@ -708,6 +710,14 @@ public class Config { flush(); } + public double getMinRelayFeeRate() { + return minRelayFeeRate; + } + + public void setMinRelayFeeRate(double minRelayFeeRate) { + this.minRelayFeeRate = minRelayFeeRate; + } + public Double getAppWidth() { return appWidth; } diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index 6a2a4b1a..141a8994 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -616,6 +616,7 @@ public class PayNymController { List opReturns = List.of(blindedPaymentCode); Double feeRate = AppServices.getDefaultFeeRate(); Double minimumFeeRate = AppServices.getMinimumFeeRate(); + Double minRelayFeeRate = AppServices.getMinimumRelayFeeRate(); boolean groupByAddress = Config.get().isGroupByAddress(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); @@ -623,7 +624,7 @@ public class PayNymController { List utxoSelectors = List.of(utxos == null ? new KnapsackUtxoSelector(noInputsFee) : new PresetUtxoSelector(utxos, true, false)); List txoFilters = List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(wallet)); - return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs); + return wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, minRelayFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs); } private Map getNotificationTransaction(PaymentCode externalPaymentCode) { diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 4fd6b76e..6a0a9c24 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.getLongFeeRatesRange().getLast()) { 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) { @@ -1225,9 +1225,17 @@ public class HeadersController extends TransactionFormController implements Init UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat(); if(failMessage.startsWith("min relay fee not met")) { - AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(AppServices.getMinimumRelayFeeRate()) + " sats/vB. " + - "This usually happens because a keystore has created a signature that is larger than necessary.\n\n" + - "You can solve this by recreating the transaction with a slightly increased fee rate."); + if(AppServices.getServerMinimumRelayFeeRate() != null && !AppServices.getServerMinimumRelayFeeRate().equals(AppServices.getMinimumRelayFeeRate())) { + AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum configured relay fee rate for the server of " + + format.getCurrencyFormat().format(AppServices.getServerMinimumRelayFeeRate()) + " sats/vB."); + } else { + Double minRelayFeeRate = AppServices.getServerMinimumRelayFeeRate() != null ? AppServices.getServerMinimumRelayFeeRate() : AppServices.getMinimumRelayFeeRate(); + AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + format.getCurrencyFormat().format(minRelayFeeRate) + " sats/vB. " + + "This usually happens because a keystore has created a signature that is larger than necessary.\n\n" + + "You can solve this by recreating the transaction with a slightly increased fee rate."); + } + } else if(failMessage.startsWith("dust")) { + AppServices.showErrorDialog("Error broadcasting transaction", "The server will not accept this transaction for broadcast due to its configured dust limit policy."); } else if(failMessage.startsWith("bad-txns-inputs-missingorspent")) { AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error indicating some or all of the UTXOs this transaction is spending are missing or have already been spent."); } else if(failMessage.contains("mempool min fee not met")) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 57f4d970..0d7ddeb1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -484,7 +484,6 @@ 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()) )); @@ -606,10 +605,11 @@ public class SendController extends WalletFormController implements Initializabl try { List payments = transactionPayments != null ? transactionPayments : getPayments(); updateOptimizationButtons(payments); - if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) { + if(!userFeeSet.get() || getFeeValueSats() != null) { Wallet wallet = getWalletForm().getWallet(); Long userFee = userFeeSet.get() ? getFeeValueSats() : null; double feeRate = getUserFeeRate(); + double minRelayFeeRate = AppServices.getMinimumRelayFeeRate(); Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); boolean groupByAddress = Config.get().isGroupByAddress(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); @@ -617,7 +617,7 @@ public class SendController extends WalletFormController implements Initializabl walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getTxoFilters(), payments, opReturnsList, excludedChangeNodes, - feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction); + feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, replacedTransaction); walletTransactionService.setOnSucceeded(event -> { if(!walletTransactionService.isIgnoreResult()) { walletTransactionProperty.setValue(walletTransactionService.getValue()); @@ -688,6 +688,7 @@ public class SendController extends WalletFormController implements Initializabl private final Set excludedChangeNodes; private final double feeRate; private final double longTermFeeRate; + private final double minRelayFeeRate; private final Long fee; private final Integer currentBlockHeight; private final boolean groupByAddress; @@ -698,7 +699,8 @@ public class SendController extends WalletFormController implements Initializabl public WalletTransactionService(Map> addressNodeMap, Wallet wallet, List utxoSelectors, List txoFilters, List payments, List opReturns, Set excludedChangeNodes, - double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) { + double feeRate, double longTermFeeRate, double minRelayFeeRate, Long fee, + Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, BlockTransaction replacedTransaction) { this.addressNodeMap = addressNodeMap; this.wallet = wallet; this.utxoSelectors = utxoSelectors; @@ -708,6 +710,7 @@ public class SendController extends WalletFormController implements Initializabl this.excludedChangeNodes = excludedChangeNodes; this.feeRate = feeRate; this.longTermFeeRate = longTermFeeRate; + this.minRelayFeeRate = minRelayFeeRate; this.fee = fee; this.currentBlockHeight = currentBlockHeight; this.groupByAddress = groupByAddress; @@ -747,7 +750,7 @@ public class SendController extends WalletFormController implements Initializabl private WalletTransaction getWalletTransaction() throws InsufficientFundsException { updateMessage("Selecting UTXOs..."); WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, - feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); + feeRate, longTermFeeRate, minRelayFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); updateMessage("Deriving keys..."); walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet()); return walletTransaction; @@ -878,7 +881,7 @@ public class SendController extends WalletFormController implements Initializabl * @return the fee rate to use when constructing a transaction */ public Double getUserFeeRate() { - return (userFeeSet.get() ? Transaction.DEFAULT_MIN_RELAY_FEE : getFeeRate()); + return (userFeeSet.get() ? AppServices.getMinimumRelayFeeRate() : getFeeRate()); } public Double getFeeRate() { @@ -942,7 +945,6 @@ public class SendController extends WalletFormController implements Initializabl private void setFeeRatePriority(Double feeRateAmt) { 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) { @@ -963,9 +965,10 @@ 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(); + Double maxFeeRate = AppServices.getFeeRatesRange().getLast(); Double highestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(0)); if(highestBlocksRate < maxFeeRate && feeRateAmt > (highestBlocksRate + ((maxFeeRate - highestBlocksRate) / 10))) { feeRatePriority.setText("Overpaid"); @@ -1243,11 +1246,13 @@ public class SendController extends WalletFormController implements Initializabl List utxoSelectors = List.of(new PresetUtxoSelector(walletTransaction.getSelectedUtxos().keySet(), true, false)); Long userFee = userFeeSet.get() ? getFeeValueSats() : null; double feeRate = getUserFeeRate(); + Double minRelayFeeRate = AppServices.getMinimumRelayFeeRate(); Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); boolean groupByAddress = Config.get().isGroupByAddress(); boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs(); - WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs); + WalletTransaction finalWalletTx = decryptedWallet.createWalletTransaction(utxoSelectors, getTxoFilters(), walletTransaction.getPayments(), List.of(blindedPaymentCode), + excludedChangeNodes, feeRate, getMinimumFeeRate(), minRelayFeeRate, userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs); PSBT psbt = finalWalletTx.createPSBT(); decryptedWallet.sign(psbt); decryptedWallet.finalise(psbt); @@ -1635,6 +1640,14 @@ public class SendController extends WalletFormController implements Initializabl recentBlocksView.updateFeeRatesSource(event.getFeeRateSource()); } + @Subscribe + public void connection(ConnectionEvent event) { + if(!Objects.equals(event.getMinimumRelayFeeRate(), event.getPreviousMinimumRelayFeeRate())) { + feeRange.updateFeeRange(event.getMinimumRelayFeeRate(), event.getPreviousMinimumRelayFeeRate()); + updateTransaction(); + } + } + private class PrivacyAnalysisTooltip extends VBox { private final List