Improve trade fee display at create and take offer screens and popups

This commit is contained in:
chimp1984 2020-12-20 18:01:19 -05:00
parent b04a56e543
commit bf720737db
No known key found for this signature in database
GPG Key ID: 9801B4EC591F90E3
5 changed files with 179 additions and 123 deletions

View File

@ -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})
####################################################################

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Volume> 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);
}
}
}

View File

@ -87,11 +87,13 @@ import javafx.beans.value.ChangeListener;
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<M extends MutableOfferDataModel> extends ActivatableWithDataModel<M> {
private final BtcValidator btcValidator;
private final BsqValidator bsqValidator;
@ -489,55 +491,28 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
private void applyMakerFee() {
Coin makerFeeAsCoin = dataModel.getMakerFee();
if (makerFeeAsCoin != null) {
isTradeFeeVisible.setValue(true);
tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin));
Coin makerFeeInBtc = dataModel.getMakerFeeInBtc();
Optional<Volume> 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<Volume> 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 abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
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 abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
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() {

View File

@ -23,6 +23,7 @@ import bisq.desktop.common.model.ViewModel;
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.btc.wallet.Restrictions;
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.collections.ObservableList;
import javafx.util.Callback;
import java.util.Optional;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@ -279,50 +277,23 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
private void applyTakerFee() {
Coin takerFeeAsCoin = dataModel.getTakerFee();
if (takerFeeAsCoin != null) {
isTradeFeeVisible.setValue(true);
tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin));
Coin makerFeeInBtc = dataModel.getTakerFeeInBtc();
Optional<Volume> 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<Volume> 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 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
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 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
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() {

View File

@ -126,7 +126,7 @@ public class DisplayUtils {
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 class DisplayUtils {
public static String getFeeWithFiatAmount(Coin makerFeeAsCoin,
Optional<Volume> 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.
* <p>0.01234 -> 0.0123