diff --git a/core/src/main/java/bisq/core/provider/fee/FeeProvider.java b/core/src/main/java/bisq/core/provider/fee/FeeProvider.java index 3a1e4f4f807..b998402b383 100644 --- a/core/src/main/java/bisq/core/provider/fee/FeeProvider.java +++ b/core/src/main/java/bisq/core/provider/fee/FeeProvider.java @@ -24,6 +24,7 @@ import bisq.network.http.HttpClient; import bisq.common.app.Version; +import bisq.common.config.Config; import bisq.common.util.Tuple2; import com.google.gson.Gson; @@ -58,8 +59,11 @@ public Tuple2, Map> getFees() throws IOException try { LinkedTreeMap dataMap = (LinkedTreeMap) linkedTreeMap.get("dataMap"); Long btcTxFee = ((Double) dataMap.get("btcTxFee")).longValue(); + Long btcMinTxFee = dataMap.get("btcMinTxFee") != null ? + ((Double) dataMap.get("btcMinTxFee")).longValue() : Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte(); map.put("BTC", btcTxFee); + map.put("btcMinTxFee", btcMinTxFee); } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); diff --git a/core/src/main/java/bisq/core/provider/fee/FeeService.java b/core/src/main/java/bisq/core/provider/fee/FeeService.java index 46ba82e4f62..0f1fc0eba74 100644 --- a/core/src/main/java/bisq/core/provider/fee/FeeService.java +++ b/core/src/main/java/bisq/core/provider/fee/FeeService.java @@ -98,6 +98,7 @@ public static Coin getMinTakerFee(boolean currencyForFeeIsBtc) { private Map timeStampMap; @Getter private long lastRequest; + @Getter private long minFeePerVByte; private long epochInSecondAtLastRequest; @@ -157,14 +158,15 @@ public void onSuccess(@Nullable Tuple2, Map> res epochInSecondAtLastRequest = timeStampMap.get("bitcoinFeesTs"); final Map map = result.second; txFeePerVbyte = map.get("BTC"); + minFeePerVByte = map.get("btcMinTxFee"); if (txFeePerVbyte < minFeePerVByte) { - log.warn("The delivered fee per vbyte is smaller than the min. default fee of 5 sat/vbyte"); + log.warn("The delivered fee of {} sat/vbyte is smaller than the min. default fee of {} sat/vbyte", txFeePerVbyte, minFeePerVByte); txFeePerVbyte = minFeePerVByte; } feeUpdateCounter.set(feeUpdateCounter.get() + 1); - log.info("BTC tx fee: txFeePerVbyte={}", txFeePerVbyte); + log.info("BTC tx fee: txFeePerVbyte={} minFeePerVbyte={}", txFeePerVbyte, minFeePerVByte); if (resultHandler != null) resultHandler.run(); }); diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 7593f31ff69..644cbf5e276 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -29,6 +29,7 @@ import bisq.core.locale.TradeCurrency; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountUtil; +import bisq.core.provider.fee.FeeService; import bisq.core.setup.CoreNetworkCapabilities; import bisq.network.p2p.network.BridgeAddressProvider; @@ -165,6 +166,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid private final PersistenceManager persistenceManager; private final Config config; + private final FeeService feeService; private final LocalBitcoinNode localBitcoinNode; private final String btcNodesFromOptions, referralIdFromOptions, rpcUserFromOptions, rpcPwFromOptions; @@ -181,6 +183,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid @Inject public Preferences(PersistenceManager persistenceManager, Config config, + FeeService feeService, LocalBitcoinNode localBitcoinNode, @Named(Config.BTC_NODES) String btcNodesFromOptions, @Named(Config.REFERRAL_ID) String referralId, @@ -191,6 +194,7 @@ public Preferences(PersistenceManager persistenceManager, this.persistenceManager = persistenceManager; this.config = config; + this.feeService = feeService; this.localBitcoinNode = localBitcoinNode; this.btcNodesFromOptions = btcNodesFromOptions; this.referralIdFromOptions = referralId; @@ -907,7 +911,7 @@ public List getBridgeAddresses() { public long getWithdrawalTxFeeInVbytes() { return Math.max(prefPayload.getWithdrawalTxFeeInVbytes(), - Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte()); + feeService.getMinFeePerVByte()); } public boolean isDaoFullNode() { diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index c5bafe23217..5643b18c7e3 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -294,7 +294,7 @@ private void initializeGeneralOptions() { String estimatedFee = String.valueOf(feeService.getTxFeePerVbyte().value); try { int withdrawalTxFeePerVbyte = Integer.parseInt(transactionFeeInputTextField.getText()); - final long minFeePerVbyte = Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte(); + final long minFeePerVbyte = feeService.getMinFeePerVByte(); if (withdrawalTxFeePerVbyte < minFeePerVbyte) { new Popup().warning(Res.get("setting.preferences.txFeeMin", minFeePerVbyte)).show(); transactionFeeInputTextField.setText(estimatedFee); diff --git a/pricenode/src/main/java/bisq/price/mining/FeeRate.java b/pricenode/src/main/java/bisq/price/mining/FeeRate.java index 19a548cfa4a..b5c25bcfecf 100644 --- a/pricenode/src/main/java/bisq/price/mining/FeeRate.java +++ b/pricenode/src/main/java/bisq/price/mining/FeeRate.java @@ -24,11 +24,13 @@ public class FeeRate { private final String currency; private final long price; + private final long minimumFee; private final long timestamp; - public FeeRate(String currency, long price, long timestamp) { + public FeeRate(String currency, long price, long minimumFee, long timestamp) { this.currency = currency; this.price = price; + this.minimumFee = minimumFee; this.timestamp = timestamp; } @@ -40,6 +42,10 @@ public long getPrice() { return price; } + public long getMinimumFee() { + return minimumFee; + } + public long getTimestamp() { return timestamp; } diff --git a/pricenode/src/main/java/bisq/price/mining/FeeRateService.java b/pricenode/src/main/java/bisq/price/mining/FeeRateService.java index 7234e636c8b..ea7f9669185 100644 --- a/pricenode/src/main/java/bisq/price/mining/FeeRateService.java +++ b/pricenode/src/main/java/bisq/price/mining/FeeRateService.java @@ -55,6 +55,7 @@ public Map getFees() { Map allFeeRates = new HashMap<>(); AtomicLong sumOfAllFeeRates = new AtomicLong(); + AtomicLong sumOfAllMinFeeRates = new AtomicLong(); AtomicInteger amountOfFeeRates = new AtomicInteger(); // Process each provider, retrieve and store their fee rate @@ -67,6 +68,7 @@ public Map getFees() { String currency = feeRate.getCurrency(); if ("BTC".equals(currency)) { sumOfAllFeeRates.getAndAdd(feeRate.getPrice()); + sumOfAllMinFeeRates.getAndAdd(feeRate.getMinimumFee()); amountOfFeeRates.getAndAdd(1); } }); @@ -75,10 +77,15 @@ public Map getFees() { long averageFeeRate = (amountOfFeeRates.intValue() > 0) ? sumOfAllFeeRates.longValue() / amountOfFeeRates.intValue() : FeeRateProvider.MIN_FEE_RATE; + long averageMinFeeRate = (amountOfFeeRates.intValue() > 0) + ? sumOfAllMinFeeRates.longValue() / amountOfFeeRates.intValue() + : FeeRateProvider.MIN_FEE_RATE; // Make sure the returned value is within the min-max range averageFeeRate = Math.max(averageFeeRate, FeeRateProvider.MIN_FEE_RATE); averageFeeRate = Math.min(averageFeeRate, FeeRateProvider.MAX_FEE_RATE); + averageMinFeeRate = Math.max(averageMinFeeRate, FeeRateProvider.MIN_FEE_RATE); + averageMinFeeRate = Math.min(averageMinFeeRate, FeeRateProvider.MAX_FEE_RATE); // Prepare response: Add timestamp of now // Since this is an average, the timestamp is associated with when the moment in @@ -87,6 +94,7 @@ public Map getFees() { // Prepare response: Add the fee average allFeeRates.put("btcTxFee", averageFeeRate); + allFeeRates.put("btcMinTxFee", averageMinFeeRate); // Build response return new HashMap<>() {{ diff --git a/pricenode/src/main/java/bisq/price/mining/providers/MempoolFeeRateProvider.java b/pricenode/src/main/java/bisq/price/mining/providers/MempoolFeeRateProvider.java index bb0c66867e4..797690e7817 100644 --- a/pricenode/src/main/java/bisq/price/mining/providers/MempoolFeeRateProvider.java +++ b/pricenode/src/main/java/bisq/price/mining/providers/MempoolFeeRateProvider.java @@ -39,7 +39,7 @@ import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; +import java.util.Set; /** * {@link FeeRateProvider} that interprets the Mempool API format to retrieve a mining @@ -77,33 +77,37 @@ public MempoolFeeRateProvider(Environment env) { protected FeeRate doGet() { // Default value is the minimum rate. If the connection to the fee estimate // provider fails, we fall back to this value. - long estimatedFeeRate = MIN_FEE_RATE; try { - estimatedFeeRate = getEstimatedFeeRate(); + return getEstimatedFeeRate(); } catch (Exception e) { // Something happened with the connection log.error("Error retrieving bitcoin mining fee estimation: " + e.getMessage()); } - return new FeeRate("BTC", estimatedFeeRate, Instant.now().getEpochSecond()); + return new FeeRate("BTC", MIN_FEE_RATE, MIN_FEE_RATE, Instant.now().getEpochSecond()); } - private long getEstimatedFeeRate() { - return getFeeRatePredictions() - .filter(p -> p.getKey().equalsIgnoreCase("halfHourFee")) - .map(Map.Entry::getValue) - .findFirst() - .map(r -> { - log.info("Retrieved estimated mining fee of {} sat/vbyte from {}", r, getMempoolApiHostname()); - return r; - }) - .map(r -> Math.max(r, MIN_FEE_RATE)) - .map(r -> Math.min(r, MAX_FEE_RATE)) - .orElse(MIN_FEE_RATE); + private FeeRate getEstimatedFeeRate() { + Set> feeRatePredictions = getFeeRatePredictions(); + long estimatedFeeRate = feeRatePredictions.stream() + .filter(p -> p.getKey().equalsIgnoreCase("halfHourFee")) + .map(Map.Entry::getValue) + .findFirst() + .map(r -> Math.max(r, MIN_FEE_RATE)) + .map(r -> Math.min(r, MAX_FEE_RATE)) + .orElse(MIN_FEE_RATE); + long minimumFee = feeRatePredictions.stream() + .filter(p -> p.getKey().equalsIgnoreCase("minimumFee")) + .map(Map.Entry::getValue) + .findFirst() + .map(r -> Math.multiplyExact(r, 2)) // multiply the minimumFee by 2 (per wiz) + .orElse(MIN_FEE_RATE); + log.info("Retrieved estimated mining fee of {} sat/vB and minimumFee of {} sat/vB from {}", estimatedFeeRate, minimumFee, getMempoolApiHostname()); + return new FeeRate("BTC", estimatedFeeRate, minimumFee, Instant.now().getEpochSecond()); } - private Stream> getFeeRatePredictions() { + private Set> getFeeRatePredictions() { return restTemplate.exchange( RequestEntity .get(UriComponentsBuilder @@ -112,7 +116,7 @@ private Stream> getFeeRatePredictions() { .build().toUri()) .build(), new ParameterizedTypeReference>() { } - ).getBody().entrySet().stream(); + ).getBody().entrySet(); } /** diff --git a/pricenode/src/test/java/bisq/price/mining/FeeRateServiceTest.java b/pricenode/src/test/java/bisq/price/mining/FeeRateServiceTest.java index ed1d196b64f..dd21a8c1339 100644 --- a/pricenode/src/test/java/bisq/price/mining/FeeRateServiceTest.java +++ b/pricenode/src/test/java/bisq/price/mining/FeeRateServiceTest.java @@ -102,7 +102,7 @@ private void doSanityChecksForRetrievedData(Map retrievedData, l assertNotEquals(0L, retrievedData.get("bitcoinFeesTs")); Map retrievedDataMap = (Map) retrievedData.get("dataMap"); - assertEquals(1, retrievedDataMap.size()); + assertEquals(2, retrievedDataMap.size()); assertEquals(expectedFeeRate, retrievedDataMap.get("btcTxFee")); } } diff --git a/pricenode/src/test/java/bisq/price/mining/providers/MempoolFeeRateProviderTest.java b/pricenode/src/test/java/bisq/price/mining/providers/MempoolFeeRateProviderTest.java index 153f35687a5..34b1ea69805 100644 --- a/pricenode/src/test/java/bisq/price/mining/providers/MempoolFeeRateProviderTest.java +++ b/pricenode/src/test/java/bisq/price/mining/providers/MempoolFeeRateProviderTest.java @@ -61,7 +61,7 @@ public static FeeRateProvider buildDummyReachableMempoolFeeRateProvider(long fee MempoolFeeRateProvider dummyProvider = new MempoolFeeRateProvider.First(env) { @Override protected FeeRate doGet() { - return new FeeRate("BTC", feeRate, Instant.now().getEpochSecond()); + return new FeeRate("BTC", feeRate, MIN_FEE_RATE, Instant.now().getEpochSecond()); } };