From bf720737db6f3c3256b2c99b50ed4ae3fea1da06 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 20 Dec 2020 18:01:19 -0500 Subject: [PATCH] Improve trade fee display at create and take offer screens and popups --- .../resources/i18n/displayStrings.properties | 14 ++- .../java/bisq/desktop/main/offer/FeeUtil.java | 82 ++++++++++++++++ .../main/offer/MutableOfferViewModel.java | 98 ++++++++----------- .../offer/takeoffer/TakeOfferViewModel.java | 96 +++++++----------- .../java/bisq/desktop/util/DisplayUtils.java | 12 +-- 5 files changed, 179 insertions(+), 123 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fdda850d637..52e492a6165 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -192,7 +192,7 @@ shared.tradeWalletBalance=Trade wallet balance shared.makerTxFee=Maker: {0} shared.takerTxFee=Taker: {0} shared.iConfirm=I confirm -shared.tradingFeeInBsqInfo=equivalent to {0} used as trading fee +shared.tradingFeeInBsqInfo=≈ {0} shared.openURL=Open {0} shared.fiat=Fiat shared.crypto=Crypto @@ -454,7 +454,6 @@ createOffer.warning.sellBelowMarketPrice=You will always get {0}% less than the createOffer.warning.buyAboveMarketPrice=You will always pay {0}% more than the current market price as the price of your offer will be continuously updated. createOffer.tradeFee.descriptionBTCOnly=Trade fee createOffer.tradeFee.descriptionBSQEnabled=Select trade fee currency -createOffer.tradeFee.fiatAndPercent=≈ {0} / {1} of trade amount # new entries createOffer.placeOfferButton=Review: Place offer to {0} bitcoin @@ -462,7 +461,14 @@ createOffer.alreadyFunded=You had already funded that offer.\nYour funds have be createOffer.createOfferFundWalletInfo.headline=Fund your offer # suppress inspection "TrailingSpacesInProperty" createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n -createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\nThose funds are reserved in your local wallet and will get locked into the multisig deposit address once someone takes your offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3}\n- Mining fee: {4}\n\nYou can choose between two options when funding your trade:\n- Use your Bisq wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup. +createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\n\ + Those funds are reserved in your local wallet and will get locked into the multisig deposit address once someone takes your offer.\n\n\ + The amount is the sum of:\n\ + {1}\ + - Your security deposit: {2}\n\ + - Trading fee: {3}\n\ + - Mining fee: {4}\n\n\ + You can choose between two options when funding your trade:\n- Use your Bisq wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup. # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) createOffer.amountPriceBox.error.message=An error occurred when placing the offer:\n\n{0}\n\n\ @@ -2702,6 +2708,8 @@ feeOptionWindow.info=You can choose to pay the trade fee in BSQ or in BTC. If yo feeOptionWindow.optionsLabel=Choose currency for trade fee payment feeOptionWindow.useBTC=Use BTC feeOptionWindow.fee={0} (≈ {1}) +feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2}) +feeOptionWindow.btcFeeWithPercentage={0} ({1}) #################################################################### diff --git a/desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java b/desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java new file mode 100644 index 00000000000..82f2363e5ed --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/FeeUtil.java @@ -0,0 +1,82 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.offer; + +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.GUIUtil; + +import bisq.core.locale.Res; +import bisq.core.monetary.Volume; +import bisq.core.offer.OfferUtil; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; + +import java.util.Optional; + +public class FeeUtil { + public static String getTradeFeeWithFiatEquivalent(OfferUtil offerUtil, + Coin tradeFee, + boolean isCurrencyForMakerFeeBtc, + CoinFormatter formatter) { + if (!isCurrencyForMakerFeeBtc && !DevEnv.isDaoActivated()) { + return ""; + } + + Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(tradeFee, + isCurrencyForMakerFeeBtc, + formatter); + + return DisplayUtils.getFeeWithFiatAmount(tradeFee, optionalBtcFeeInFiat, formatter); + } + + public static String getTradeFeeWithFiatEquivalentAndPercentage(OfferUtil offerUtil, + Coin tradeFee, + Coin tradeAmount, + boolean isCurrencyForMakerFeeBtc, + CoinFormatter formatter, + Coin minTradeFee) { + if (isCurrencyForMakerFeeBtc) { + String feeAsBtc = formatter.formatCoinWithCode(tradeFee); + String percentage; + if (!tradeFee.isGreaterThan(minTradeFee)) { + percentage = Res.get("guiUtil.requiredMinimum") + .replace("(", "") + .replace(")", ""); + } else { + percentage = GUIUtil.getPercentage(tradeFee, tradeAmount) + + " " + Res.get("guiUtil.ofTradeAmount"); + } + return offerUtil.getFeeInUserFiatCurrency(tradeFee, + isCurrencyForMakerFeeBtc, + formatter) + .map(DisplayUtils::formatAverageVolumeWithCode) + .map(feeInFiat -> Res.get("feeOptionWindow.btcFeeWithFiatAndPercentage", feeAsBtc, feeInFiat, percentage)) + .orElseGet(() -> Res.get("feeOptionWindow.btcFeeWithPercentage", feeAsBtc, percentage)); + } else { + // For BSQ we use the fiat equivalent only. Calculating the % value would be more effort. + // We could calculate the BTC value if the BSQ fee and use that... + return FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + tradeFee, + false, + formatter); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java index 83bd3d9f741..0b0566988b1 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -87,11 +87,13 @@ import javafx.util.Callback; -import java.util.Optional; import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + import static javafx.beans.binding.Bindings.createStringBinding; +@Slf4j public abstract class MutableOfferViewModel extends ActivatableWithDataModel { private final BtcValidator btcValidator; private final BsqValidator bsqValidator; @@ -489,55 +491,28 @@ private void createListeners() { } private void applyMakerFee() { - Coin makerFeeAsCoin = dataModel.getMakerFee(); - if (makerFeeAsCoin != null) { - isTradeFeeVisible.setValue(true); - - tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin)); - - Coin makerFeeInBtc = dataModel.getMakerFeeInBtc(); - Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc, - true, - bsqFormatter); - String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter); - if (DevEnv.isDaoActivated()) { - tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount); - } else { - tradeFeeInBtcWithFiat.set(btcFormatter.formatCoinWithCode(makerFeeAsCoin)); - } - - Coin makerFeeInBsq = dataModel.getMakerFeeInBsq(); - Optional optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq, - false, - bsqFormatter); - String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, - optionalBsqFeeInFiat, - bsqFormatter); - if (DevEnv.isDaoActivated()) { - tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount); - } else { - // Before DAO is enabled we show fee as fiat and % in second line - String feeInFiatAsString; - if (optionalBtcFeeInFiat != null && optionalBtcFeeInFiat.isPresent()) { - feeInFiatAsString = DisplayUtils.formatVolumeWithCode(optionalBtcFeeInFiat.get()); - } else { - feeInFiatAsString = Res.get("shared.na"); - } - - double amountAsDouble = (double) dataModel.getAmount().get().value; - double makerFeeInBtcAsDouble = (double) makerFeeInBtc.value; - double percent = makerFeeInBtcAsDouble / amountAsDouble; - - tradeFeeInBsqWithFiat.set(Res.get("createOffer.tradeFee.fiatAndPercent", - feeInFiatAsString, - FormattingUtils.formatToPercentWithSymbol(percent))); - } - } tradeFeeCurrencyCode.set(dataModel.isCurrencyForMakerFeeBtc() ? Res.getBaseCurrencyCode() : "BSQ"); tradeFeeDescription.set(DevEnv.isDaoActivated() ? Res.get("createOffer.tradeFee.descriptionBSQEnabled") : Res.get("createOffer.tradeFee.descriptionBTCOnly")); + + Coin makerFeeAsCoin = dataModel.getMakerFee(); + if (makerFeeAsCoin == null) { + return; + } + + isTradeFeeVisible.setValue(true); + tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin)); + tradeFeeInBtcWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getMakerFeeInBtc(), + true, + btcFormatter)); + tradeFeeInBsqWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getMakerFeeInBsq(), + false, + bsqFormatter)); } + private void updateMarketPriceAvailable() { marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); @@ -985,15 +960,23 @@ public String getSecurityDepositWithCode() { return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()); } + public String getTradeFee() { - //TODO use last bisq market price to estimate BSQ val - final Coin makerFeeAsCoin = dataModel.getMakerFee(); - final String makerFee = getFormatterForMakerFee().formatCoinWithCode(makerFeeAsCoin); - if (dataModel.isCurrencyForMakerFeeBtc()) - return makerFee + GUIUtil.getPercentageOfTradeAmount(makerFeeAsCoin, dataModel.getAmount().get(), + if (dataModel.isCurrencyForMakerFeeBtc()) { + return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + dataModel.getMakerFeeInBtc(), + dataModel.getAmount().get(), + true, + btcFormatter, FeeService.getMinMakerFee(dataModel.isCurrencyForMakerFeeBtc())); - else - return makerFee + " (" + Res.get("shared.tradingFeeInBsqInfo", btcFormatter.formatCoinWithCode(makerFeeAsCoin)) + ")"; + } else { + // For BSQ we use the fiat equivalent only. Calculating the % value would require to + // calculate the BTC value of the BSQ fee and use that... + return FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getMakerFeeInBsq(), + false, + bsqFormatter); + } } public String getMakerFeePercentage() { @@ -1025,10 +1008,13 @@ public String getFundsStructure() { } public String getTxFee() { - Coin txFeeAsCoin = dataModel.getTxFee(); - return btcFormatter.formatCoinWithCode(txFeeAsCoin) + - GUIUtil.getPercentageOfTradeAmount(txFeeAsCoin, dataModel.getAmount().get(), Coin.ZERO); - + return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + dataModel.getTxFee(), + dataModel.getAmount().get(), + true, + btcFormatter, + Coin.ZERO + ); } public String getTxFeePercentage() { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 0bf3855eb24..e7f7b3b6cc8 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -23,6 +23,7 @@ import bisq.desktop.main.MainView; import bisq.desktop.main.funds.FundsView; import bisq.desktop.main.funds.deposit.DepositView; +import bisq.desktop.main.offer.FeeUtil; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; @@ -33,7 +34,6 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.monetary.Price; -import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferRestrictions; @@ -76,8 +76,6 @@ import javafx.util.Callback; -import java.util.Optional; - import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; @@ -279,50 +277,23 @@ public void setIsCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) { } private void applyTakerFee() { - Coin takerFeeAsCoin = dataModel.getTakerFee(); - if (takerFeeAsCoin != null) { - isTradeFeeVisible.setValue(true); - - tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin)); - - Coin makerFeeInBtc = dataModel.getTakerFeeInBtc(); - Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc, - true, - bsqFormatter); - String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter); - if (DevEnv.isDaoActivated()) { - tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount); - } else { - tradeFeeInBtcWithFiat.set(btcFormatter.formatCoinWithCode(takerFeeAsCoin)); - } - - Coin makerFeeInBsq = dataModel.getTakerFeeInBsq(); - Optional optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq, - false, - bsqFormatter); - String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, optionalBsqFeeInFiat, bsqFormatter); - if (DevEnv.isDaoActivated()) { - tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount); - } else { - // Before DAO is enabled we show fee as fiat and % in second line - String feeInFiatAsString; - if (optionalBtcFeeInFiat != null && optionalBtcFeeInFiat.isPresent()) { - feeInFiatAsString = DisplayUtils.formatVolumeWithCode(optionalBtcFeeInFiat.get()); - } else { - feeInFiatAsString = Res.get("shared.na"); - } - - double amountAsLong = (double) dataModel.getAmount().get().value; - double makerFeeInBtcAsLong = (double) makerFeeInBtc.value; - double percent = makerFeeInBtcAsLong / amountAsLong; - - tradeFeeInBsqWithFiat.set(Res.get("createOffer.tradeFee.fiatAndPercent", - feeInFiatAsString, - FormattingUtils.formatToPercentWithSymbol(percent))); - } - } tradeFeeDescription.set(DevEnv.isDaoActivated() ? Res.get("createOffer.tradeFee.descriptionBSQEnabled") : Res.get("createOffer.tradeFee.descriptionBTCOnly")); + Coin takerFeeAsCoin = dataModel.getTakerFee(); + if (takerFeeAsCoin == null) { + return; + } + + isTradeFeeVisible.setValue(true); + tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin)); + tradeFeeInBtcWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getTakerFeeInBtc(), + true, + btcFormatter)); + tradeFeeInBsqWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getTakerFeeInBsq(), + false, + bsqFormatter)); } @@ -706,14 +677,21 @@ public String getSecurityDepositWithCode() { } public String getTradeFee() { - //TODO use last bisq market price to estimate BSQ val - final Coin takerFeeAsCoin = dataModel.getTakerFee(); - final String takerFee = getFormatterForTakerFee().formatCoinWithCode(takerFeeAsCoin); - if (dataModel.isCurrencyForTakerFeeBtc()) - return takerFee + GUIUtil.getPercentageOfTradeAmount(takerFeeAsCoin, dataModel.getAmount().get(), - FeeService.getMinTakerFee(dataModel.isCurrencyForTakerFeeBtc())); - else - return takerFee + " (" + Res.get("shared.tradingFeeInBsqInfo", btcFormatter.formatCoinWithCode(takerFeeAsCoin)) + ")"; + if (dataModel.isCurrencyForTakerFeeBtc()) { + return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + dataModel.getTakerFeeInBtc(), + dataModel.getAmount().get(), + true, + btcFormatter, + FeeService.getMinMakerFee(dataModel.isCurrencyForTakerFeeBtc())); + } else { + // For BSQ we use the fiat equivalent only. Calculating the % value would require to + // calculate the BTC value of the BSQ fee and use that... + return FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil, + dataModel.getTakerFeeInBsq(), + false, + bsqFormatter); + } } public String getTakerFeePercentage() { @@ -733,11 +711,13 @@ public String getTotalToPayInfo() { } public String getTxFee() { - Coin txFeeAsCoin = dataModel.getTotalTxFee(); - return btcFormatter.formatCoinWithCode(txFeeAsCoin) + - GUIUtil.getPercentageOfTradeAmount(txFeeAsCoin, dataModel.getAmount().get(), - Coin.ZERO); - + return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, + dataModel.getTotalTxFee(), + dataModel.getAmount().get(), + true, + btcFormatter, + Coin.ZERO + ); } public String getTxFeePercentage() { diff --git a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java index 83a0d157de3..f8d144e6fa9 100644 --- a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java @@ -126,7 +126,7 @@ public static String formatVolumeWithCode(Volume volume) { return formatVolume(volume, FIAT_VOLUME_FORMAT, true); } - static String formatAverageVolumeWithCode(Volume volume) { + public static String formatAverageVolumeWithCode(Volume volume) { return formatVolume(volume, FIAT_VOLUME_FORMAT.minDecimals(2), true); } @@ -257,16 +257,16 @@ public static String formatPrice(Price price, Boolean decimalAligned, int maxPla public static String getFeeWithFiatAmount(Coin makerFeeAsCoin, Optional optionalFeeInFiat, CoinFormatter formatter) { - String fee = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na"); - String feeInFiatAsString; + String feeInBtc = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na"); if (optionalFeeInFiat != null && optionalFeeInFiat.isPresent()) { - feeInFiatAsString = formatAverageVolumeWithCode(optionalFeeInFiat.get()); + String feeInFiat = formatAverageVolumeWithCode(optionalFeeInFiat.get()); + return Res.get("feeOptionWindow.fee", feeInBtc, feeInFiat); } else { - feeInFiatAsString = Res.get("shared.na"); + return feeInBtc; } - return Res.get("feeOptionWindow.fee", fee, feeInFiatAsString); } + /** * Converts to a coin with max. 4 decimal places. Last place gets rounded. *

0.01234 -> 0.0123