diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index 0d2ae126beb..3c7d5ced9df 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -17,21 +17,16 @@ package bisq.core.offer; -import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.TxFeeEstimationService; -import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; -import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.monetary.Price; -import bisq.core.payment.HalCashAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountUtil; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; -import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.Preferences; import bisq.core.user.User; import bisq.core.util.coin.CoinUtil; @@ -62,14 +57,9 @@ @Slf4j @Singleton public class CreateOfferService { + private final OfferUtil offerUtil; private final TxFeeEstimationService txFeeEstimationService; - private final MakerFeeProvider makerFeeProvider; - private final BsqWalletService bsqWalletService; - private final Preferences preferences; private final PriceFeedService priceFeedService; - private final AccountAgeWitnessService accountAgeWitnessService; - private final ReferralIdService referralIdService; - private final FilterManager filterManager; private final P2PService p2PService; private final PubKeyRing pubKeyRing; private final User user; @@ -81,26 +71,16 @@ public class CreateOfferService { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public CreateOfferService(TxFeeEstimationService txFeeEstimationService, - MakerFeeProvider makerFeeProvider, - BsqWalletService bsqWalletService, - Preferences preferences, + public CreateOfferService(OfferUtil offerUtil, + TxFeeEstimationService txFeeEstimationService, PriceFeedService priceFeedService, - AccountAgeWitnessService accountAgeWitnessService, - ReferralIdService referralIdService, - FilterManager filterManager, P2PService p2PService, PubKeyRing pubKeyRing, User user, BtcWalletService btcWalletService) { + this.offerUtil = offerUtil; this.txFeeEstimationService = txFeeEstimationService; - this.makerFeeProvider = makerFeeProvider; - this.bsqWalletService = bsqWalletService; - this.preferences = preferences; this.priceFeedService = priceFeedService; - this.accountAgeWitnessService = accountAgeWitnessService; - this.referralIdService = referralIdService; - this.filterManager = filterManager; this.p2PService = p2PService; this.pubKeyRing = pubKeyRing; this.user = user; @@ -161,7 +141,7 @@ public Offer createAndGetOffer(String offerId, NodeAddress makerAddress = p2PService.getAddress(); boolean useMarketBasedPriceValue = useMarketBasedPrice && isMarketPriceAvailable(currencyCode) && - !isHalCashAccount(paymentAccount); + !paymentAccount.isHalCashAccount(); long priceAsLong = price != null && !useMarketBasedPriceValue ? price.getValue() : 0L; double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0; @@ -185,11 +165,11 @@ public Offer createAndGetOffer(String offerId, double sellerSecurityDeposit = getSellerSecurityDepositAsDouble(buyerSecurityDepositAsDouble); Coin txFeeFromFeeService = getEstimatedFeeAndTxSize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first; Coin txFeeToUse = txFee.isPositive() ? txFee : txFeeFromFeeService; - Coin makerFeeAsCoin = getMakerFee(amount); - boolean isCurrencyForMakerFeeBtc = OfferUtil.isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount); + Coin makerFeeAsCoin = offerUtil.getMakerFee(amount); + boolean isCurrencyForMakerFeeBtc = offerUtil.isCurrencyForMakerFeeBtc(amount); Coin buyerSecurityDepositAsCoin = getBuyerSecurityDeposit(amount, buyerSecurityDepositAsDouble); Coin sellerSecurityDepositAsCoin = getSellerSecurityDeposit(amount, sellerSecurityDeposit); - long maxTradeLimit = getMaxTradeLimit(paymentAccount, currencyCode, direction); + long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction); long maxTradePeriod = paymentAccount.getMaxTradePeriod(); // reserved for future use cases @@ -200,15 +180,11 @@ public Offer createAndGetOffer(String offerId, long lowerClosePrice = 0; long upperClosePrice = 0; String hashOfChallenge = null; - Map extraDataMap = OfferUtil.getExtraDataMap(accountAgeWitnessService, - referralIdService, - paymentAccount, + Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, - preferences, direction); - OfferUtil.validateOfferData(filterManager, - p2PService, + offerUtil.validateOfferData( buyerSecurityDepositAsDouble, paymentAccount, currencyCode, @@ -261,8 +237,12 @@ public Tuple2 getEstimatedFeeAndTxSize(Coin amount, OfferPayload.Direction direction, double buyerSecurityDeposit, double sellerSecurityDeposit) { - Coin reservedFundsForOffer = getReservedFundsForOffer(direction, amount, buyerSecurityDeposit, sellerSecurityDeposit); - return txFeeEstimationService.getEstimatedFeeAndTxSizeForMaker(reservedFundsForOffer, getMakerFee(amount)); + Coin reservedFundsForOffer = getReservedFundsForOffer(direction, + amount, + buyerSecurityDeposit, + sellerSecurityDeposit); + return txFeeEstimationService.getEstimatedFeeAndTxSizeForMaker(reservedFundsForOffer, + offerUtil.getMakerFee(amount)); } public Coin getReservedFundsForOffer(OfferPayload.Direction direction, @@ -274,7 +254,7 @@ public Coin getReservedFundsForOffer(OfferPayload.Direction direction, amount, buyerSecurityDeposit, sellerSecurityDeposit); - if (!isBuyOffer(direction)) + if (!offerUtil.isBuyOffer(direction)) reservedFundsForOffer = reservedFundsForOffer.add(amount); return reservedFundsForOffer; @@ -284,7 +264,7 @@ public Coin getSecurityDeposit(OfferPayload.Direction direction, Coin amount, double buyerSecurityDeposit, double sellerSecurityDeposit) { - return isBuyOffer(direction) ? + return offerUtil.isBuyOffer(direction) ? getBuyerSecurityDeposit(amount, buyerSecurityDeposit) : getSellerSecurityDeposit(amount, sellerSecurityDeposit); } @@ -294,25 +274,6 @@ public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) { Restrictions.getSellerSecurityDepositAsPercent(); } - public Coin getMakerFee(Coin amount) { - return makerFeeProvider.getMakerFee(bsqWalletService, preferences, amount); - } - - public long getMaxTradeLimit(PaymentAccount paymentAccount, - String currencyCode, - OfferPayload.Direction direction) { - if (paymentAccount != null) { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction); - } else { - return 0; - } - } - - public boolean isBuyOffer(OfferPayload.Direction direction) { - return OfferUtil.isBuyOffer(direction); - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -322,20 +283,13 @@ private boolean isMarketPriceAvailable(String currencyCode) { return marketPrice != null && marketPrice.isExternallyProvidedPrice(); } - private boolean isHalCashAccount(PaymentAccount paymentAccount) { - return paymentAccount instanceof HalCashAccount; - } - private Coin getBuyerSecurityDeposit(Coin amount, double buyerSecurityDeposit) { Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(buyerSecurityDeposit, amount); return getBoundedBuyerSecurityDeposit(percentOfAmountAsCoin); } private Coin getSellerSecurityDeposit(Coin amount, double sellerSecurityDeposit) { - Coin amountAsCoin = amount; - if (amountAsCoin == null) - amountAsCoin = Coin.ZERO; - + Coin amountAsCoin = (amount == null) ? Coin.ZERO : amount; Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(sellerSecurityDeposit, amountAsCoin); return getBoundedSellerSecurityDeposit(percentOfAmountAsCoin); } diff --git a/core/src/main/java/bisq/core/offer/MakerFeeProvider.java b/core/src/main/java/bisq/core/offer/MakerFeeProvider.java deleted file mode 100644 index dc2d0fbd7f2..00000000000 --- a/core/src/main/java/bisq/core/offer/MakerFeeProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.core.offer; - -import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.user.Preferences; - -import org.bitcoinj.core.Coin; - -public class MakerFeeProvider { - public Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, Coin amount) { - return OfferUtil.getMakerFee(bsqWalletService, preferences, amount); - } -} diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java index 93bab10b419..bcca0078b4e 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -27,6 +27,7 @@ import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; +import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; @@ -96,13 +97,13 @@ public enum State { private final OfferPayload offerPayload; @JsonExclude @Getter - transient private ObjectProperty stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN); + final transient private ObjectProperty stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN); @JsonExclude @Nullable transient private OfferAvailabilityProtocol availabilityProtocol; @JsonExclude @Getter - transient private StringProperty errorMessageProperty = new SimpleStringProperty(); + final transient private StringProperty errorMessageProperty = new SimpleStringProperty(); @JsonExclude @Nullable @Setter @@ -231,9 +232,9 @@ public Volume getVolumeByAmount(Coin amount) { if (price != null && amount != null) { Volume volumeByAmount = price.getVolumeByAmount(amount); if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) - volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount); + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(offerPayload.getCurrencyCode())) - volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount); + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); return volumeByAmount; } else { diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index d5c47ae473d..373df679073 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -19,7 +19,6 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.btc.wallet.Restrictions; import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -44,7 +43,8 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; -import com.google.common.annotations.VisibleForTesting; +import javax.inject.Inject; +import javax.inject.Singleton; import java.util.HashMap; import java.util.Map; @@ -54,95 +54,174 @@ import javax.annotation.Nullable; +import static bisq.common.util.MathUtils.roundDoubleToLong; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; +import static bisq.core.btc.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent; +import static bisq.core.btc.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent; +import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput; +import static bisq.core.btc.wallet.Restrictions.isDust; +import static bisq.core.offer.OfferPayload.*; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** - * This class holds utility methods for the creation of an Offer. - * Most of these are extracted here because they are used both in the GUI and in the API. - *

- * Long-term there could be a GUI-agnostic OfferService which provides these and other functionality to both the - * GUI and the API. + * This class holds utility methods for creating, editing and taking an Offer. */ @Slf4j +@Singleton public class OfferUtil { + private final AccountAgeWitnessService accountAgeWitnessService; + private final BsqWalletService bsqWalletService; + private final FilterManager filterManager; + private final Preferences preferences; + private final PriceFeedService priceFeedService; + private final P2PService p2PService; + private final ReferralIdService referralIdService; + + @Inject + public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, + BsqWalletService bsqWalletService, + FilterManager filterManager, + Preferences preferences, + PriceFeedService priceFeedService, + P2PService p2PService, + ReferralIdService referralIdService) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.bsqWalletService = bsqWalletService; + this.filterManager = filterManager; + this.preferences = preferences; + this.priceFeedService = priceFeedService; + this.p2PService = p2PService; + this.referralIdService = referralIdService; + } + /** * Given the direction, is this a BUY? * * @param direction the offer direction - * @return {@code true} for an offer to buy BTC from the taker, {@code false} for an offer to sell BTC to the taker + * @return {@code true} for an offer to buy BTC from the taker, {@code false} for an + * offer to sell BTC to the taker */ - public static boolean isBuyOffer(OfferPayload.Direction direction) { - return direction == OfferPayload.Direction.BUY; + public boolean isBuyOffer(Direction direction) { + return direction == Direction.BUY; + } + + public long getMaxTradeLimit(PaymentAccount paymentAccount, + String currencyCode, + Direction direction) { + return paymentAccount != null + ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction) + : 0; } /** - * Returns the makerFee as Coin, this can be priced in BTC or BSQ. + * Return true if a balance can cover a cost. * - * @param bsqWalletService wallet service used to check if there is enough BSQ to pay the fee - * @param preferences preferences are used to see if the user indicated a preference for paying fees in BTC - * @param amount the amount of BTC to trade - * @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null} + * @param cost the cost of a trade + * @param balance a wallet balance + * @return true if balance >= cost */ - @Nullable - public static Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, @Nullable Coin amount) { - boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount); - return getMakerFee(isCurrencyForMakerFeeBtc, amount); + public boolean isBalanceSufficient(Coin cost, Coin balance) { + return cost != null && balance.compareTo(cost) >= 0; } /** - * Calculates the maker fee for the given amount, marketPrice and marketPriceMargin. + * Return the wallet balance shortage for a given trade cost, or zero if there is + * no shortage. * - * @param isCurrencyForMakerFeeBtc {@code true} to pay fee in BTC, {@code false} to pay fee in BSQ - * @param amount the amount of BTC to trade - * @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null} + * @param cost the cost of a trade + * @param balance a wallet balance + * @return the wallet balance shortage for the given cost, else zero. */ - @Nullable - public static Coin getMakerFee(boolean isCurrencyForMakerFeeBtc, @Nullable Coin amount) { - if (amount != null) { - Coin feePerBtc = CoinUtil.getFeePerBtc(FeeService.getMakerFeePerBtc(isCurrencyForMakerFeeBtc), amount); - return CoinUtil.maxCoin(feePerBtc, FeeService.getMinMakerFee(isCurrencyForMakerFeeBtc)); + public Coin getBalanceShortage(Coin cost, Coin balance) { + if (cost != null) { + Coin shortage = cost.subtract(balance); + return shortage.isNegative() ? Coin.ZERO : shortage; } else { - return null; + return Coin.ZERO; } } /** - * Checks if the maker fee should be paid in BTC, this can be the case due to user preference or because the user - * doesn't have enough BSQ. + * Returns the usable BSQ balance. + * + * @return Coin the usable BSQ balance + */ + public Coin getUsableBsqBalance() { + // We have to keep a minimum amount of BSQ == bitcoin dust limit, otherwise there + // would be dust violations for change UTXOs; essentially means the minimum usable + // balance of BSQ is 5.46. + Coin usableBsqBalance = bsqWalletService.getAvailableConfirmedBalance().subtract(getMinNonDustOutput()); + return usableBsqBalance.isNegative() ? Coin.ZERO : usableBsqBalance; + } + + public double calculateManualPrice(double volumeAsDouble, double amountAsDouble) { + return volumeAsDouble / amountAsDouble; + } + + public double calculateMarketPriceMargin(double manualPrice, double marketPrice) { + return MathUtils.roundDouble(manualPrice / marketPrice, 4); + } + + /** + * Returns the makerFee as Coin, this can be priced in BTC or BSQ. * - * @param preferences preferences are used to see if the user indicated a preference for paying fees in BTC - * @param bsqWalletService wallet service used to check if there is enough BSQ to pay the fee * @param amount the amount of BTC to trade - * @return {@code true} if BTC is preferred or the trade amount is nonnull and there isn't enough BSQ for it + * @return the maker fee for the given trade amount, or {@code null} if the amount + * is {@code null} */ - public static boolean isCurrencyForMakerFeeBtc(Preferences preferences, - BsqWalletService bsqWalletService, - @Nullable Coin amount) { + @Nullable + public Coin getMakerFee(@Nullable Coin amount) { + boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(amount); + return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount); + } + + public Coin getTxFeeBySize(Coin txFeePerByteFromFeeService, int sizeInBytes) { + return txFeePerByteFromFeeService.multiply(getAverageTakerFeeTxSize(sizeInBytes)); + } + + // We use the sum of the size of the trade fee and the deposit tx to get an average. + // Miners will take the trade fee tx if the total fee of both dependent txs are good + // enough. With that we avoid that we overpay in case that the trade fee has many + // inputs and we would apply that fee for the other 2 txs as well. We still might + // overpay a bit for the payout tx. + public int getAverageTakerFeeTxSize(int txSize) { + return (txSize + 320) / 2; + } + + /** + * Checks if the maker fee should be paid in BTC, this can be the case due to user + * preference or because the user doesn't have enough BSQ. + * + * @param amount the amount of BTC to trade + * @return {@code true} if BTC is preferred or the trade amount is nonnull and there + * isn't enough BSQ for it. + */ + public boolean isCurrencyForMakerFeeBtc(@Nullable Coin amount) { boolean payFeeInBtc = preferences.getPayFeeInBtc(); - boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(bsqWalletService, amount); + boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(amount); return payFeeInBtc || !bsqForFeeAvailable; } /** * Checks if the available BSQ balance is sufficient to pay for the offer's maker fee. * - * @param bsqWalletService wallet service used to check if there is enough BSQ to pay the fee * @param amount the amount of BTC to trade * @return {@code true} if the balance is sufficient, {@code false} otherwise */ - public static boolean isBsqForMakerFeeAvailable(BsqWalletService bsqWalletService, @Nullable Coin amount) { + public boolean isBsqForMakerFeeAvailable(@Nullable Coin amount) { Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance(); - Coin makerFee = getMakerFee(false, amount); + Coin makerFee = CoinUtil.getMakerFee(false, amount); - // If we don't know yet the maker fee (amount is not set) we return true, otherwise we would disable BSQ - // fee each time we open the create offer screen as there the amount is not set. + // If we don't know yet the maker fee (amount is not set) we return true, + // otherwise we would disable BSQ fee each time we open the create offer screen + // as there the amount is not set. if (makerFee == null) return true; Coin surplusFunds = availableBalance.subtract(makerFee); - if (Restrictions.isDust(surplusFunds)) { + if (isDust(surplusFunds)) { return false; // we can't be left with dust } return !availableBalance.subtract(makerFee).isNegative(); @@ -150,7 +229,7 @@ public static boolean isBsqForMakerFeeAvailable(BsqWalletService bsqWalletServic @Nullable - public static Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, @Nullable Coin amount) { + public Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, @Nullable Coin amount) { if (amount != null) { Coin feePerBtc = CoinUtil.getFeePerBtc(FeeService.getTakerFeePerBtc(isCurrencyForTakerFeeBtc), amount); return CoinUtil.maxCoin(feePerBtc, FeeService.getMinTakerFee(isCurrencyForTakerFeeBtc)); @@ -159,238 +238,117 @@ public static Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, @Nullable Coin } } - public static boolean isCurrencyForTakerFeeBtc(Preferences preferences, - BsqWalletService bsqWalletService, - Coin amount) { + public boolean isCurrencyForTakerFeeBtc(Coin amount) { boolean payFeeInBtc = preferences.getPayFeeInBtc(); - boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(bsqWalletService, amount); + boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(amount); return payFeeInBtc || !bsqForFeeAvailable; } - public static boolean isBsqForTakerFeeAvailable(BsqWalletService bsqWalletService, @Nullable Coin amount) { + public boolean isBsqForTakerFeeAvailable(@Nullable Coin amount) { Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance(); Coin takerFee = getTakerFee(false, amount); - // If we don't know yet the maker fee (amount is not set) we return true, otherwise we would disable BSQ - // fee each time we open the create offer screen as there the amount is not set. + // If we don't know yet the maker fee (amount is not set) we return true, + // otherwise we would disable BSQ fee each time we open the create offer screen + // as there the amount is not set. if (takerFee == null) return true; Coin surplusFunds = availableBalance.subtract(takerFee); - if (Restrictions.isDust(surplusFunds)) { + if (isDust(surplusFunds)) { return false; // we can't be left with dust } return !availableBalance.subtract(takerFee).isNegative(); } - public static Volume getRoundedFiatVolume(Volume volumeByAmount) { - // We want to get rounded to 1 unit of the fiat currency, e.g. 1 EUR. - return getAdjustedFiatVolume(volumeByAmount, 1); - } - - public static Volume getAdjustedVolumeForHalCash(Volume volumeByAmount) { - // EUR has precision 4 and we want multiple of 10 so we divide by 100000 then - // round and multiply with 10 - return getAdjustedFiatVolume(volumeByAmount, 10); - } - - /** - * - * @param volumeByAmount The volume generated from an amount - * @param factor The factor used for rounding. E.g. 1 means rounded to units of 1 EUR, 10 means rounded to 10 EUR... - * @return The adjusted Fiat volume - */ - @VisibleForTesting - static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) { - // Fiat currencies use precision 4 and we want multiple of factor so we divide by 10000 * factor then - // round and multiply with factor - long roundedVolume = Math.round((double) volumeByAmount.getValue() / (10000d * factor)) * factor; - // Smallest allowed volume is factor (e.g. 10 EUR or 1 EUR,...) - roundedVolume = Math.max(factor, roundedVolume); - return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); - } - - /** - * Calculate the possibly adjusted amount for {@code amount}, taking into account the - * {@code price} and {@code maxTradeLimit} and {@code factor}. - * - * @param amount Bitcoin amount which is a candidate for getting rounded. - * @param price Price used in relation to that amount. - * @param maxTradeLimit The max. trade limit of the users account, in satoshis. - * @return The adjusted amount - */ - public static Coin getRoundedFiatAmount(Coin amount, Price price, long maxTradeLimit) { - return getAdjustedAmount(amount, price, maxTradeLimit, 1); - } - - public static Coin getAdjustedAmountForHalCash(Coin amount, Price price, long maxTradeLimit) { - return getAdjustedAmount(amount, price, maxTradeLimit, 10); - } - - /** - * Calculate the possibly adjusted amount for {@code amount}, taking into account the - * {@code price} and {@code maxTradeLimit} and {@code factor}. - * - * @param amount Bitcoin amount which is a candidate for getting rounded. - * @param price Price used in relation to that amount. - * @param maxTradeLimit The max. trade limit of the users account, in satoshis. - * @param factor The factor used for rounding. E.g. 1 means rounded to units of - * 1 EUR, 10 means rounded to 10 EUR, etc. - * @return The adjusted amount - */ - @VisibleForTesting - static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int factor) { - checkArgument( - amount.getValue() >= 10_000, - "amount needs to be above minimum of 10k satoshis" - ); - checkArgument( - factor > 0, - "factor needs to be positive" - ); - // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or - // 10 EUR in case of HalCash. - Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode()); - if (smallestUnitForVolume.getValue() <= 0) - return Coin.ZERO; - - Coin smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume); - long minTradeAmount = Restrictions.getMinTradeAmount().value; - - // We use 10 000 satoshi as min allowed amount - checkArgument( - minTradeAmount >= 10_000, - "MinTradeAmount must be at least 10k satoshis" - ); - smallestUnitForAmount = Coin.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.value)); - // We don't allow smaller amount values than smallestUnitForAmount - if (amount.compareTo(smallestUnitForAmount) < 0) - amount = smallestUnitForAmount; - - // We get the adjusted volume from our amount - Volume volume = getAdjustedFiatVolume(price.getVolumeByAmount(amount), factor); - if (volume.getValue() <= 0) - return Coin.ZERO; - - // From that adjusted volume we calculate back the amount. It might be a bit different as - // the amount used as input before due rounding. - amount = price.getAmountByVolume(volume); - - // For the amount we allow only 4 decimal places - long adjustedAmount = Math.round((double) amount.value / 10000d) * 10000; - - // If we are above our trade limit we reduce the amount by the smallestUnitForAmount - while (adjustedAmount > maxTradeLimit) { - adjustedAmount -= smallestUnitForAmount.value; - } - adjustedAmount = Math.max(minTradeAmount, adjustedAmount); - adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); - return Coin.valueOf(adjustedAmount); - } - - public static Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc, - Preferences preferences, PriceFeedService priceFeedService, - CoinFormatter bsqFormatter) { + public Optional getFeeInUserFiatCurrency(Coin makerFee, + boolean isCurrencyForMakerFeeBtc, + CoinFormatter bsqFormatter) { String countryCode = preferences.getUserCountry().code; String userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode(); return getFeeInUserFiatCurrency(makerFee, isCurrencyForMakerFeeBtc, userCurrencyCode, - priceFeedService, bsqFormatter); } - private static Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc, - String userCurrencyCode, PriceFeedService priceFeedService, - CoinFormatter bsqFormatter) { - // We use the users currency derived from his selected country. - // We don't use the preferredTradeCurrency from preferences as that can be also set to an altcoin. - - MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode); - if (marketPrice != null && makerFee != null) { - long marketPriceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); - Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong); - - if (isCurrencyForMakerFeeBtc) { - return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee)); - } else { - Optional optionalBsqPrice = priceFeedService.getBsqPrice(); - if (optionalBsqPrice.isPresent()) { - Price bsqPrice = optionalBsqPrice.get(); - String inputValue = bsqFormatter.formatCoin(makerFee); - Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ"); - Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume); - return Optional.of(userCurrencyPrice.getVolumeByAmount(requiredBtc)); - } else { - return Optional.empty(); - } - } - } else { - return Optional.empty(); - } - } - - - public static Map getExtraDataMap(AccountAgeWitnessService accountAgeWitnessService, - ReferralIdService referralIdService, - PaymentAccount paymentAccount, - String currencyCode, - Preferences preferences, - OfferPayload.Direction direction) { + public Map getExtraDataMap(PaymentAccount paymentAccount, + String currencyCode, + Direction direction) { Map extraDataMap = new HashMap<>(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { - String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); - extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); + String myWitnessHashAsHex = accountAgeWitnessService + .getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); + extraDataMap.put(ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); } if (referralIdService.getOptionalReferralId().isPresent()) { - extraDataMap.put(OfferPayload.REFERRAL_ID, referralIdService.getOptionalReferralId().get()); + extraDataMap.put(REFERRAL_ID, referralIdService.getOptionalReferralId().get()); } if (paymentAccount instanceof F2FAccount) { - extraDataMap.put(OfferPayload.F2F_CITY, ((F2FAccount) paymentAccount).getCity()); - extraDataMap.put(OfferPayload.F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); + extraDataMap.put(F2F_CITY, ((F2FAccount) paymentAccount).getCity()); + extraDataMap.put(F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); } - extraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList()); + extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList()); - if (currencyCode.equals("XMR") && direction == OfferPayload.Direction.SELL) { + if (currencyCode.equals("XMR") && direction == Direction.SELL) { preferences.getAutoConfirmSettingsList().stream() .filter(e -> e.getCurrencyCode().equals("XMR")) .filter(AutoConfirmSettings::isEnabled) - .forEach(e -> extraDataMap.put(OfferPayload.XMR_AUTO_CONF, OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE)); + .forEach(e -> extraDataMap.put(XMR_AUTO_CONF, XMR_AUTO_CONF_ENABLED_VALUE)); } return extraDataMap.isEmpty() ? null : extraDataMap; } - public static void validateOfferData(FilterManager filterManager, - P2PService p2PService, - double buyerSecurityDeposit, - PaymentAccount paymentAccount, - String currencyCode, - Coin makerFeeAsCoin) { + public void validateOfferData(double buyerSecurityDeposit, + PaymentAccount paymentAccount, + String currencyCode, + Coin makerFeeAsCoin) { checkNotNull(makerFeeAsCoin, "makerFee must not be null"); checkNotNull(p2PService.getAddress(), "Address must not be null"); - checkArgument(buyerSecurityDeposit <= Restrictions.getMaxBuyerSecurityDepositAsPercent(), + checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(), "securityDeposit must not exceed " + - Restrictions.getMaxBuyerSecurityDepositAsPercent()); - checkArgument(buyerSecurityDeposit >= Restrictions.getMinBuyerSecurityDepositAsPercent(), + getMaxBuyerSecurityDepositAsPercent()); + checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(), "securityDeposit must not be less than " + - Restrictions.getMinBuyerSecurityDepositAsPercent()); + getMinBuyerSecurityDepositAsPercent()); checkArgument(!filterManager.isCurrencyBanned(currencyCode), Res.get("offerbook.warning.currencyBanned")); checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), Res.get("offerbook.warning.paymentMethodBanned")); } - // TODO no code duplication found in UI code (added for API) - /* public static Coin getFundsNeededForOffer(Coin tradeAmount, Coin buyerSecurityDeposit, OfferPayload.Direction direction) { - boolean buyOffer = isBuyOffer(direction); - Coin needed = buyOffer ? buyerSecurityDeposit : Restrictions.getSellerSecurityDeposit(); - if (!buyOffer) - needed = needed.add(tradeAmount); + private Optional getFeeInUserFiatCurrency(Coin makerFee, + boolean isCurrencyForMakerFeeBtc, + String userCurrencyCode, + CoinFormatter bsqFormatter) { + // We use the users currency derived from his selected country. We don't use the + // preferredTradeCurrency from preferences as that can be also set to an altcoin. + MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode); + if (marketPrice != null && makerFee != null) { + long marketPriceAsLong = roundDoubleToLong( + scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); + Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong); - return needed; - }*/ + if (isCurrencyForMakerFeeBtc) { + return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee)); + } else { + Optional optionalBsqPrice = priceFeedService.getBsqPrice(); + if (optionalBsqPrice.isPresent()) { + Price bsqPrice = optionalBsqPrice.get(); + String inputValue = bsqFormatter.formatCoin(makerFee); + Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ"); + Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume); + return Optional.of(userCurrencyPrice.getVolumeByAmount(requiredBtc)); + } else { + return Optional.empty(); + } + } + } else { + return Optional.empty(); + } + } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java index 12a9565b710..b38649ef942 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -126,8 +126,7 @@ public void addCurrency(TradeCurrency tradeCurrency) { } public void removeCurrency(TradeCurrency tradeCurrency) { - if (tradeCurrencies.contains(tradeCurrency)) - tradeCurrencies.remove(tradeCurrency); + tradeCurrencies.remove(tradeCurrency); } public boolean hasMultipleCurrencies() { @@ -174,6 +173,30 @@ public String getOwnerId() { return paymentAccountPayload.getOwnerId(); } + public boolean isHalCashAccount() { + return this instanceof HalCashAccount; + } + + /** + * Return an Optional of the trade currency for this payment account, or + * Optional.empty() if none is found. If this payment account has a selected + * trade currency, that is returned, else its single trade currency is returned, + * else the first trade currency in the this payment account's tradeCurrencies + * list is returned. + * + * @return Optional of the trade currency for the given payment account + */ + public Optional getTradeCurrency() { + if (this.getSelectedTradeCurrency() != null) + return Optional.of(this.getSelectedTradeCurrency()); + else if (this.getSingleTradeCurrency() != null) + return Optional.of(this.getSingleTradeCurrency()); + else if (!this.getTradeCurrencies().isEmpty()) + return Optional.of(this.getTradeCurrencies().get(0)); + else + return Optional.empty(); + } + public void onAddToUser() { // We are in the process to get added to the user. This is called just before saving the account and the // last moment we could apply some special handling if needed (e.g. as it happens for Revolut) diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java index 174719b0651..81602d226d0 100644 --- a/core/src/main/java/bisq/core/trade/Contract.java +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -21,10 +21,10 @@ import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferUtil; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; +import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; @@ -231,9 +231,9 @@ public Volume getTradeVolume() { Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount()); if (getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) - volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount); + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(getOfferPayload().getCurrencyCode())) - volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount); + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); return volumeByAmount; } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index bb73bc6ea96..a6f2d56ab99 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -22,7 +22,6 @@ import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; -import bisq.core.offer.OfferUtil; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; @@ -32,6 +31,7 @@ import bisq.core.trade.protocol.ProcessModel; import bisq.core.trade.protocol.ProcessModelServiceProvider; import bisq.core.trade.txproof.AssetTxProofResult; +import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; @@ -623,13 +623,11 @@ public void initialize(ProcessModelServiceProvider serviceProvider) { arbitratorPubKeyRing = arbitrator.getPubKeyRing(); }); - serviceProvider.getMediatorManager().getDisputeAgentByNodeAddress(mediatorNodeAddress).ifPresent(mediator -> { - mediatorPubKeyRing = mediator.getPubKeyRing(); - }); + serviceProvider.getMediatorManager().getDisputeAgentByNodeAddress(mediatorNodeAddress) + .ifPresent(mediator -> mediatorPubKeyRing = mediator.getPubKeyRing()); - serviceProvider.getRefundAgentManager().getDisputeAgentByNodeAddress(refundAgentNodeAddress).ifPresent(refundAgent -> { - refundAgentPubKeyRing = refundAgent.getPubKeyRing(); - }); + serviceProvider.getRefundAgentManager().getDisputeAgentByNodeAddress(refundAgentNodeAddress) + .ifPresent(refundAgent -> refundAgentPubKeyRing = refundAgent.getPubKeyRing()); isInitialized = true; } @@ -831,9 +829,9 @@ public Volume getTradeVolume() { Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount()); if (offer != null) { if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) - volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount); + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) - volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount); + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); } return volumeByAmount; } else { @@ -864,15 +862,15 @@ private long getTradeStartTime() { if (depositTx.getConfidence().getDepthInBlocks() > 0) { final long tradeTime = getTakeOfferDate().getTime(); // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime() - long blockTime = depositTx.getIncludedInBestChainAt() != null ? depositTx.getIncludedInBestChainAt().getTime() : depositTx.getUpdateTime().getTime(); + long blockTime = depositTx.getIncludedInBestChainAt() != null + ? depositTx.getIncludedInBestChainAt().getTime() + : depositTx.getUpdateTime().getTime(); // If block date is in future (Date in Bitcoin blocks can be off by +/- 2 hours) we use our current date. // If block date is earlier than our trade date we use our trade date. if (blockTime > now) startTime = now; - else if (blockTime < tradeTime) - startTime = tradeTime; else - startTime = blockTime; + startTime = Math.max(blockTime, tradeTime); log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}", new Date(startTime), new Date(tradeTime), new Date(blockTime)); @@ -929,13 +927,9 @@ public boolean isFundsLockedIn() { // In refund agent case the funds are spent anyway with the time locked payout. We do not consider that as // locked in funds. - if (disputeState == DisputeState.REFUND_REQUESTED || - disputeState == DisputeState.REFUND_REQUEST_STARTED_BY_PEER || - disputeState == DisputeState.REFUND_REQUEST_CLOSED) { - return false; - } - - return true; + return disputeState != DisputeState.REFUND_REQUESTED && + disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER && + disputeState != DisputeState.REFUND_REQUEST_CLOSED; } public boolean isDepositConfirmed() { diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java index 0b7830a34cb..864b08f9677 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -23,8 +23,8 @@ import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferUtil; import bisq.core.trade.Trade; +import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; @@ -310,7 +310,7 @@ public Volume getTradeVolume() { return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); } else { Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); - return OfferUtil.getRoundedFiatVolume(volume); + return VolumeUtil.getRoundedFiatVolume(volume); } } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java index a3f3e996a07..8f4cf09d154 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics3.java @@ -23,8 +23,8 @@ import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferUtil; import bisq.core.trade.Trade; +import bisq.core.util.VolumeUtil; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; @@ -355,7 +355,7 @@ public Volume getTradeVolume() { return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount())); } else { Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount())); - return OfferUtil.getRoundedFiatVolume(volume); + return VolumeUtil.getRoundedFiatVolume(volume); } } diff --git a/core/src/main/java/bisq/core/util/VolumeUtil.java b/core/src/main/java/bisq/core/util/VolumeUtil.java new file mode 100644 index 00000000000..71712bd3657 --- /dev/null +++ b/core/src/main/java/bisq/core/util/VolumeUtil.java @@ -0,0 +1,50 @@ +/* + * 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.core.util; + +import bisq.core.monetary.Volume; + +public class VolumeUtil { + + public static Volume getRoundedFiatVolume(Volume volumeByAmount) { + // We want to get rounded to 1 unit of the fiat currency, e.g. 1 EUR. + return getAdjustedFiatVolume(volumeByAmount, 1); + } + + public static Volume getAdjustedVolumeForHalCash(Volume volumeByAmount) { + // EUR has precision 4 and we want multiple of 10 so we divide by 100000 then + // round and multiply with 10 + return getAdjustedFiatVolume(volumeByAmount, 10); + } + + /** + * + * @param volumeByAmount The volume generated from an amount + * @param factor The factor used for rounding. E.g. 1 means rounded to + * units of 1 EUR, 10 means rounded to 10 EUR. + * @return The adjusted Fiat volume + */ + public static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) { + // Fiat currencies use precision 4 and we want multiple of factor so we divide by 10000 * factor then + // round and multiply with factor + long roundedVolume = Math.round((double) volumeByAmount.getValue() / (10000d * factor)) * factor; + // Smallest allowed volume is factor (e.g. 10 EUR or 1 EUR,...) + roundedVolume = Math.max(factor, roundedVolume); + return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); + } +} diff --git a/core/src/main/java/bisq/core/util/coin/CoinUtil.java b/core/src/main/java/bisq/core/util/coin/CoinUtil.java index d6c90d9e364..17e0195aad1 100644 --- a/core/src/main/java/bisq/core/util/coin/CoinUtil.java +++ b/core/src/main/java/bisq/core/util/coin/CoinUtil.java @@ -17,10 +17,22 @@ package bisq.core.util.coin; +import bisq.core.btc.wallet.Restrictions; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.provider.fee.FeeService; + import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; +import com.google.common.annotations.VisibleForTesting; + +import javax.annotation.Nullable; + +import static bisq.core.util.VolumeUtil.getAdjustedFiatVolume; +import static com.google.common.base.Preconditions.checkArgument; + public class CoinUtil { // Get the fee per amount @@ -75,4 +87,101 @@ public static Coin getPercentOfAmountAsCoin(double percent, Coin amount) { double amountAsDouble = amount != null ? (double) amount.value : 0; return Coin.valueOf(Math.round(percent * amountAsDouble)); } + + + /** + * Calculates the maker fee for the given amount, marketPrice and marketPriceMargin. + * + * @param isCurrencyForMakerFeeBtc {@code true} to pay fee in BTC, {@code false} to pay fee in BSQ + * @param amount the amount of BTC to trade + * @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null} + */ + @Nullable + public static Coin getMakerFee(boolean isCurrencyForMakerFeeBtc, @Nullable Coin amount) { + if (amount != null) { + Coin feePerBtc = getFeePerBtc(FeeService.getMakerFeePerBtc(isCurrencyForMakerFeeBtc), amount); + return maxCoin(feePerBtc, FeeService.getMinMakerFee(isCurrencyForMakerFeeBtc)); + } else { + return null; + } + } + + /** + * Calculate the possibly adjusted amount for {@code amount}, taking into account the + * {@code price} and {@code maxTradeLimit} and {@code factor}. + * + * @param amount Bitcoin amount which is a candidate for getting rounded. + * @param price Price used in relation to that amount. + * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @return The adjusted amount + */ + public static Coin getRoundedFiatAmount(Coin amount, Price price, long maxTradeLimit) { + return getAdjustedAmount(amount, price, maxTradeLimit, 1); + } + + public static Coin getAdjustedAmountForHalCash(Coin amount, Price price, long maxTradeLimit) { + return getAdjustedAmount(amount, price, maxTradeLimit, 10); + } + + /** + * Calculate the possibly adjusted amount for {@code amount}, taking into account the + * {@code price} and {@code maxTradeLimit} and {@code factor}. + * + * @param amount Bitcoin amount which is a candidate for getting rounded. + * @param price Price used in relation to that amount. + * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @param factor The factor used for rounding. E.g. 1 means rounded to units of + * 1 EUR, 10 means rounded to 10 EUR, etc. + * @return The adjusted amount + */ + @VisibleForTesting + static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int factor) { + checkArgument( + amount.getValue() >= 10_000, + "amount needs to be above minimum of 10k satoshis" + ); + checkArgument( + factor > 0, + "factor needs to be positive" + ); + // Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or + // 10 EUR in case of HalCash. + Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode()); + if (smallestUnitForVolume.getValue() <= 0) + return Coin.ZERO; + + Coin smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume); + long minTradeAmount = Restrictions.getMinTradeAmount().value; + + // We use 10 000 satoshi as min allowed amount + checkArgument( + minTradeAmount >= 10_000, + "MinTradeAmount must be at least 10k satoshis" + ); + smallestUnitForAmount = Coin.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.value)); + // We don't allow smaller amount values than smallestUnitForAmount + boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; + + // We get the adjusted volume from our amount + Volume volume = useSmallestUnitForAmount + ? getAdjustedFiatVolume(price.getVolumeByAmount(smallestUnitForAmount), factor) + : getAdjustedFiatVolume(price.getVolumeByAmount(amount), factor); + if (volume.getValue() <= 0) + return Coin.ZERO; + + // From that adjusted volume we calculate back the amount. It might be a bit different as + // the amount used as input before due rounding. + Coin amountByVolume = price.getAmountByVolume(volume); + + // For the amount we allow only 4 decimal places + long adjustedAmount = Math.round((double) amountByVolume.value / 10000d) * 10000; + + // If we are above our trade limit we reduce the amount by the smallestUnitForAmount + while (adjustedAmount > maxTradeLimit) { + adjustedAmount -= smallestUnitForAmount.value; + } + adjustedAmount = Math.max(minTradeAmount, adjustedAmount); + adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); + return Coin.valueOf(adjustedAmount); + } } diff --git a/core/src/test/java/bisq/core/util/CoinCryptoUtilsTest.java b/core/src/test/java/bisq/core/util/CoinCryptoUtilsTest.java deleted file mode 100644 index 2f04b7a75f4..00000000000 --- a/core/src/test/java/bisq/core/util/CoinCryptoUtilsTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.core.util; - -import bisq.core.util.coin.CoinUtil; - -import org.bitcoinj.core.Coin; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class CoinCryptoUtilsTest { - private static final Logger log = LoggerFactory.getLogger(CoinCryptoUtilsTest.class); - - @Test - public void testGetFeePerBtc() { - assertEquals(Coin.parseCoin("1"), CoinUtil.getFeePerBtc(Coin.parseCoin("1"), Coin.parseCoin("1"))); - assertEquals(Coin.parseCoin("0.1"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); - assertEquals(Coin.parseCoin("0.01"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("0.1"))); - assertEquals(Coin.parseCoin("0.015"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.3"), Coin.parseCoin("0.05"))); - } - - @Test - public void testMinCoin() { - assertEquals(Coin.parseCoin("1"), CoinUtil.minCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); - assertEquals(Coin.parseCoin("0.1"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); - assertEquals(Coin.parseCoin("0.01"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); - assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); - assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); - } - - @Test - public void testMaxCoin() { - assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); - assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); - assertEquals(Coin.parseCoin("0.1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); - assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); - assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); - } - -} diff --git a/core/src/test/java/bisq/core/offer/OfferUtilTest.java b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java similarity index 57% rename from core/src/test/java/bisq/core/offer/OfferUtilTest.java rename to core/src/test/java/bisq/core/util/coin/CoinUtilTest.java index 2c7093d1ccc..d4c2e683ef0 100644 --- a/core/src/test/java/bisq/core/offer/OfferUtilTest.java +++ b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java @@ -15,62 +15,90 @@ * along with Bisq. If not, see . */ -package bisq.core.offer; +package bisq.core.util.coin; import bisq.core.monetary.Price; import org.bitcoinj.core.Coin; -import org.junit.Assert; import org.junit.Test; -public class OfferUtilTest { +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class CoinUtilTest { + + @Test + public void testGetFeePerBtc() { + assertEquals(Coin.parseCoin("1"), CoinUtil.getFeePerBtc(Coin.parseCoin("1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.1"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.01"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("0.1"))); + assertEquals(Coin.parseCoin("0.015"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.3"), Coin.parseCoin("0.05"))); + } + + @Test + public void testMinCoin() { + assertEquals(Coin.parseCoin("1"), CoinUtil.minCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.1"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.01"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); + assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); + assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); + } + + @Test + public void testMaxCoin() { + assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1"))); + assertEquals(Coin.parseCoin("0.1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01"))); + assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05"))); + assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0"))); + } @Test public void testGetAdjustedAmount() { - Coin result = OfferUtil.getAdjustedAmount( + Coin result = CoinUtil.getAdjustedAmount( Coin.valueOf(100_000), Price.valueOf("USD", 1000_0000), 20_000_000, 1); - Assert.assertEquals( + assertEquals( "Minimum trade amount allowed should be adjusted to the smallest trade allowed.", "0.001 BTC", result.toFriendlyString() ); try { - OfferUtil.getAdjustedAmount( + CoinUtil.getAdjustedAmount( Coin.ZERO, Price.valueOf("USD", 1000_0000), 20_000_000, 1); - Assert.fail("Expected IllegalArgumentException to be thrown when amount is too low."); + fail("Expected IllegalArgumentException to be thrown when amount is too low."); } catch (IllegalArgumentException iae) { - Assert.assertEquals( + assertEquals( "Unexpected exception message.", "amount needs to be above minimum of 10k satoshis", iae.getMessage() ); } - result = OfferUtil.getAdjustedAmount( + result = CoinUtil.getAdjustedAmount( Coin.valueOf(1_000_000), Price.valueOf("USD", 1000_0000), 20_000_000, 1); - Assert.assertEquals( + assertEquals( "Minimum allowed trade amount should not be adjusted.", "0.01 BTC", result.toFriendlyString() ); - result = OfferUtil.getAdjustedAmount( + result = CoinUtil.getAdjustedAmount( Coin.valueOf(100_000), Price.valueOf("USD", 1000_0000), 1_000_000, 1); - Assert.assertEquals( + assertEquals( "Minimum trade amount allowed should respect maxTradeLimit and factor, if possible.", "0.001 BTC", result.toFriendlyString() @@ -81,12 +109,12 @@ public void testGetAdjustedAmount() { // max trade limit is 5k sat = 0.00005 BTC. But the returned amount 0.00005 BTC, or // 0.05 USD worth, which is below the factor of 1 USD, but does respect the maxTradeLimit. // Basically the given constraints (maxTradeLimit vs factor) are impossible to both fulfill.. - result = OfferUtil.getAdjustedAmount( + result = CoinUtil.getAdjustedAmount( Coin.valueOf(100_000), Price.valueOf("USD", 1000_0000), 5_000, 1); - Assert.assertEquals( + assertEquals( "Minimum trade amount allowed with low maxTradeLimit should still respect that limit, even if result does not respect the factor specified.", "0.00005 BTC", result.toFriendlyString() diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MakerFeeProvider.java b/desktop/src/main/java/bisq/desktop/main/offer/MakerFeeProvider.java deleted file mode 100644 index 2243a9e9b06..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/offer/MakerFeeProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package bisq.desktop.main.offer; - -import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.offer.OfferUtil; -import bisq.core.user.Preferences; - -import org.bitcoinj.core.Coin; - -public class MakerFeeProvider { - public Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, Coin amount) { - return OfferUtil.getMakerFee(bsqWalletService, preferences, amount); - } -} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index d035956cc73..91506f00a11 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -38,7 +38,6 @@ import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; -import bisq.core.payment.HalCashAccount; import bisq.core.payment.PaymentAccount; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; @@ -48,6 +47,7 @@ import bisq.core.user.Preferences; import bisq.core.user.User; import bisq.core.util.FormattingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; @@ -85,6 +85,7 @@ import java.util.Date; import java.util.HashSet; import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -103,7 +104,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs private final AccountAgeWitnessService accountAgeWitnessService; private final FeeService feeService; private final CoinFormatter btcFormatter; - private final MakerFeeProvider makerFeeProvider; private final Navigation navigation; private final String offerId; private final BalanceListener btcBalanceListener; @@ -133,6 +133,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs protected boolean allowAmountUpdate = true; private final TradeStatisticsManager tradeStatisticsManager; + private final Predicate> isPositiveAmount = (c) -> c.get() != null && !c.get().isZero(); + private final Predicate> isPositivePrice = (p) -> p.get() != null && !p.get().isZero(); + private final Predicate> isPositiveVolume = (v) -> v.get() != null && !v.get().isZero(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -141,6 +144,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs @Inject public MutableOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, + OfferUtil offerUtil, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, Preferences preferences, @@ -150,10 +154,9 @@ public MutableOfferDataModel(CreateOfferService createOfferService, AccountAgeWitnessService accountAgeWitnessService, FeeService feeService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - MakerFeeProvider makerFeeProvider, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { - super(btcWalletService); + super(btcWalletService, offerUtil); this.createOfferService = createOfferService; this.openOfferManager = openOfferManager; @@ -165,7 +168,6 @@ public MutableOfferDataModel(CreateOfferService createOfferService, this.accountAgeWitnessService = accountAgeWitnessService; this.feeService = feeService; this.btcFormatter = btcFormatter; - this.makerFeeProvider = makerFeeProvider; this.navigation = navigation; this.tradeStatisticsManager = tradeStatisticsManager; @@ -373,16 +375,9 @@ private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) { } } - private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) { - if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) { - if (paymentAccount.getSelectedTradeCurrency() != null) - tradeCurrency = paymentAccount.getSelectedTradeCurrency(); - else if (paymentAccount.getSingleTradeCurrency() != null) - tradeCurrency = paymentAccount.getSingleTradeCurrency(); - else if (!paymentAccount.getTradeCurrencies().isEmpty()) - tradeCurrency = paymentAccount.getTradeCurrencies().get(0); - } + if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) + tradeCurrency = paymentAccount.getTradeCurrency().orElse(tradeCurrency); checkNotNull(tradeCurrency, "tradeCurrency must not be null"); tradeCurrencyCode.set(tradeCurrency.getCode()); @@ -406,7 +401,8 @@ void onCurrencySelected(TradeCurrency tradeCurrency) { priceFeedService.setCurrencyCode(code); - Optional tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable().stream().filter(e -> e.getCode().equals(code)).findAny(); + Optional tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable() + .stream().filter(e -> e.getCode().equals(code)).findAny(); if (!tradeCurrencyOptional.isPresent()) { if (CurrencyUtil.isCryptoCurrency(code)) { CurrencyUtil.getCryptoCurrency(code).ifPresent(preferences::addCryptoCurrency); @@ -512,8 +508,8 @@ long getMaxTradeLimit() { /////////////////////////////////////////////////////////////////////////////////////////// double calculateMarketPriceManual(double marketPrice, double volumeAsDouble, double amountAsDouble) { - double manualPriceAsDouble = volumeAsDouble / amountAsDouble; - double percentage = MathUtils.roundDouble(manualPriceAsDouble / marketPrice, 4); + double manualPriceAsDouble = offerUtil.calculateManualPrice(volumeAsDouble, amountAsDouble); + double percentage = offerUtil.calculateMarketPriceMargin(manualPriceAsDouble, marketPrice); setMarketPriceMargin(percentage); @@ -521,10 +517,7 @@ long getMaxTradeLimit() { } void calculateVolume() { - if (price.get() != null && - amount.get() != null && - !amount.get().isZero() && - !price.get().isZero()) { + if (isPositivePrice.test(price) && isPositiveAmount.test(amount)) { try { Volume volumeByAmount = calculateVolumeForAmount(amount); @@ -540,10 +533,7 @@ void calculateVolume() { } void calculateMinVolume() { - if (price.get() != null && - minAmount.get() != null && - !minAmount.get().isZero() && - !price.get().isZero()) { + if (isPositivePrice.test(price) && isPositiveAmount.test(minAmount)) { try { Volume volumeByAmount = calculateVolumeForAmount(minAmount); @@ -559,25 +549,21 @@ private Volume calculateVolumeForAmount(ObjectProperty minAmount) { Volume volumeByAmount = price.get().getVolumeByAmount(minAmount.get()); // For HalCash we want multiple of 10 EUR - if (isHalCashAccount()) - volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount); + if (paymentAccount.isHalCashAccount()) + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount); + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); return volumeByAmount; } void calculateAmount() { - if (volume.get() != null && - price.get() != null && - !volume.get().isZero() && - !price.get().isZero() && - allowAmountUpdate) { + if (isPositivePrice.test(price) && isPositiveVolume.test(volume) && allowAmountUpdate) { try { Coin value = DisplayUtils.reduceTo4Decimals(price.get().getAmountByVolume(volume.get()), btcFormatter); - if (isHalCashAccount()) - value = OfferUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit()); + if (paymentAccount.isHalCashAccount()) + value = CoinUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit()); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - value = OfferUtil.getRoundedFiatAmount(value, price.get(), getMaxTradeLimit()); + value = CoinUtil.getRoundedFiatAmount(value, price.get(), getMaxTradeLimit()); calculateVolume(); @@ -608,8 +594,8 @@ Coin getSecurityDeposit() { return isBuyOffer() ? getBuyerSecurityDepositAsCoin() : getSellerSecurityDepositAsCoin(); } - public boolean isBuyOffer() { - return OfferUtil.isBuyOffer(getDirection()); + boolean isBuyOffer() { + return offerUtil.isBuyOffer(getDirection()); } public Coin getTxFee() { @@ -645,7 +631,7 @@ void setBuyerSecurityDeposit(double value) { } protected boolean isUseMarketBasedPriceValue() { - return marketPriceAvailable && useMarketBasedPrice.get() && !isHalCashAccount(); + return marketPriceAvailable && useMarketBasedPrice.get() && !paymentAccount.isHalCashAccount(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -720,13 +706,7 @@ ReadOnlyObjectProperty totalToPayAsCoinProperty() { } Coin getUsableBsqBalance() { - // we have to keep a minimum amount of BSQ == bitcoin dust limit - // otherwise there would be dust violations for change UTXOs - // essentially means the minimum usable balance of BSQ is 5.46 - Coin usableBsqBalance = bsqWalletService.getAvailableConfirmedBalance().subtract(Restrictions.getMinNonDustOutput()); - if (usableBsqBalance.isNegative()) - usableBsqBalance = Coin.ZERO; - return usableBsqBalance; + return offerUtil.getUsableBsqBalance(); } public void setMarketPriceAvailable(boolean marketPriceAvailable) { @@ -734,23 +714,23 @@ public void setMarketPriceAvailable(boolean marketPriceAvailable) { } public Coin getMakerFee(boolean isCurrencyForMakerFeeBtc) { - return OfferUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount.get()); + return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount.get()); } public Coin getMakerFee() { - return makerFeeProvider.getMakerFee(bsqWalletService, preferences, amount.get()); + return offerUtil.getMakerFee(amount.get()); } public Coin getMakerFeeInBtc() { - return OfferUtil.getMakerFee(true, amount.get()); + return CoinUtil.getMakerFee(true, amount.get()); } public Coin getMakerFeeInBsq() { - return OfferUtil.getMakerFee(false, amount.get()); + return CoinUtil.getMakerFee(false, amount.get()); } public boolean isCurrencyForMakerFeeBtc() { - return OfferUtil.isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount.get()); + return offerUtil.isCurrencyForMakerFeeBtc(amount.get()); } boolean isPreferredFeeCurrencyBtc() { @@ -758,11 +738,7 @@ boolean isPreferredFeeCurrencyBtc() { } boolean isBsqForFeeAvailable() { - return OfferUtil.isBsqForMakerFeeAvailable(bsqWalletService, amount.get()); - } - - public boolean isHalCashAccount() { - return paymentAccount instanceof HalCashAccount; + return offerUtil.isBsqForMakerFeeAvailable(amount.get()); } boolean canPlaceOffer() { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index 8924495327b..c0dab4099ce 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -874,7 +874,7 @@ protected void updatePriceToggle() { int marketPriceAvailableValue = model.marketPriceAvailableProperty.get(); if (marketPriceAvailableValue > -1) { boolean showPriceToggle = marketPriceAvailableValue == 1 && - !model.getDataModel().isHalCashAccount(); + !model.getDataModel().paymentAccount.isHalCashAccount(); percentagePriceBox.setVisible(showPriceToggle); priceTypeToggleButton.setVisible(showPriceToggle); boolean fixedPriceSelected = !model.getDataModel().getUseMarketBasedPrice().get() || !showPriceToggle; 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 248da70bda1..36718c7ba89 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -54,8 +54,10 @@ import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.CoinUtil; import bisq.core.util.validation.InputValidator; import bisq.common.Timer; @@ -63,7 +65,6 @@ import bisq.common.app.DevEnv; import bisq.common.util.MathUtils; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; @@ -96,7 +97,7 @@ public abstract class MutableOfferViewModel ext private final BsqValidator bsqValidator; protected final SecurityDepositValidator securityDepositValidator; private final PriceFeedService priceFeedService; - private AccountAgeWitnessService accountAgeWitnessService; + private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final Preferences preferences; protected final CoinFormatter btcFormatter; @@ -104,9 +105,9 @@ public abstract class MutableOfferViewModel ext private final FiatVolumeValidator fiatVolumeValidator; private final FiatPriceValidator fiatPriceValidator; private final AltcoinValidator altcoinValidator; + protected final OfferUtil offerUtil; private String amountDescription; - private String directionLabel; private String addressAsString; private final String paymentLabel; private boolean createOfferRequested; @@ -156,9 +157,6 @@ public abstract class MutableOfferViewModel ext final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); - // Those are needed for the addressTextField - private final ObjectProperty

address = new SimpleObjectProperty<>(); - private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; private ChangeListener priceStringListener, marketPriceMarginStringListener; @@ -172,7 +170,6 @@ public abstract class MutableOfferViewModel ext private ChangeListener securityDepositAsDoubleListener; private ChangeListener isWalletFundedListener; - //private ChangeListener feeFromFundingTxListener; private ChangeListener errorMessageListener; private Offer offer; private Timer timeoutTimer; @@ -201,7 +198,8 @@ public MutableOfferViewModel(M dataModel, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - BsqFormatter bsqFormatter) { + BsqFormatter bsqFormatter, + OfferUtil offerUtil) { super(dataModel); this.fiatVolumeValidator = fiatVolumeValidator; @@ -216,12 +214,12 @@ public MutableOfferViewModel(M dataModel, this.preferences = preferences; this.btcFormatter = btcFormatter; this.bsqFormatter = bsqFormatter; + this.offerUtil = offerUtil; paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); if (dataModel.getAddressEntry() != null) { addressAsString = dataModel.getAddressEntry().getAddressString(); - address.set(dataModel.getAddressEntry().getAddress()); } createListeners(); } @@ -498,8 +496,9 @@ private void applyMakerFee() { tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin)); Coin makerFeeInBtc = dataModel.getMakerFeeInBtc(); - Optional optionalBtcFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBtc, - true, preferences, priceFeedService, bsqFormatter); + Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc, + true, + bsqFormatter); String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter); if (DevEnv.isDaoActivated()) { tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount); @@ -508,9 +507,12 @@ private void applyMakerFee() { } Coin makerFeeInBsq = dataModel.getMakerFeeInBsq(); - Optional optionalBsqFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBsq, - false, preferences, priceFeedService, bsqFormatter); - String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, optionalBsqFeeInFiat, bsqFormatter); + Optional optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq, + false, + bsqFormatter); + String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, + optionalBsqFeeInFiat, + bsqFormatter); if (DevEnv.isDaoActivated()) { tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount); } else { @@ -604,7 +606,6 @@ boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurren btcValidator.setMinValue(Restrictions.getMinTradeAmount()); final boolean isBuy = dataModel.getDirection() == OfferPayload.Direction.BUY; - directionLabel = isBuy ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin"); amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); @@ -833,9 +834,7 @@ public void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newVa } // We want to trigger a recalculation of the volume - UserThread.execute(() -> { - onFocusOutVolumeTextField(true, false); - }); + UserThread.execute(() -> onFocusOutVolumeTextField(true, false)); } void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { @@ -849,10 +848,10 @@ void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { Volume volume = dataModel.getVolume().get(); if (volume != null) { // For HalCash we want multiple of 10 EUR - if (dataModel.isHalCashAccount()) - volume = OfferUtil.getAdjustedVolumeForHalCash(volume); + if (dataModel.paymentAccount.isHalCashAccount()) + volume = VolumeUtil.getAdjustedVolumeForHalCash(volume); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - volume = OfferUtil.getRoundedFiatVolume(volume); + volume = VolumeUtil.getRoundedFiatVolume(volume); this.volume.set(DisplayUtils.formatVolume(volume)); } @@ -1045,10 +1044,6 @@ public String getAmountDescription() { return amountDescription; } - public String getDirectionLabel() { - return directionLabel; - } - public String getAddressAsString() { return addressAsString; } @@ -1057,10 +1052,6 @@ public String getPaymentLabel() { return paymentLabel; } - public String formatCoin(Coin coin) { - return btcFormatter.formatCoin(coin); - } - public Offer createAndGetOffer() { offer = dataModel.createAndGetOffer(); return offer; @@ -1086,10 +1077,10 @@ private void setAmountToModel() { long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); if (price != null) { - if (dataModel.isHalCashAccount()) - amount = OfferUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); + if (dataModel.paymentAccount.isHalCashAccount()) + amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - amount = OfferUtil.getRoundedFiatAmount(amount, price, maxTradeLimit); + amount = CoinUtil.getRoundedFiatAmount(amount, price, maxTradeLimit); } dataModel.setAmount(amount); if (syncMinAmountWithAmount || @@ -1110,10 +1101,10 @@ private void setMinAmountToModel() { Price price = dataModel.getPrice().get(); long maxTradeLimit = dataModel.getMaxTradeLimit(); if (price != null) { - if (dataModel.isHalCashAccount()) - minAmount = OfferUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit); + if (dataModel.paymentAccount.isHalCashAccount()) + minAmount = CoinUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - minAmount = OfferUtil.getRoundedFiatAmount(minAmount, price, maxTradeLimit); + minAmount = CoinUtil.getRoundedFiatAmount(minAmount, price, maxTradeLimit); } dataModel.setMinAmount(minAmount); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferDataModel.java index c40cbc4f3d2..14e26e1451d 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/OfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferDataModel.java @@ -21,6 +21,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.OfferUtil; import org.bitcoinj.core.Coin; @@ -31,13 +32,17 @@ import lombok.Getter; +import static bisq.core.util.coin.CoinUtil.minCoin; + /** * Domain for that UI element. - * Note that the create offer domain has a deeper scope in the application domain (TradeManager). - * That model is just responsible for the domain specific parts displayed needed in that UI element. + * Note that the create offer domain has a deeper scope in the application domain + * (TradeManager). That model is just responsible for the domain specific parts displayed + * needed in that UI element. */ public abstract class OfferDataModel extends ActivatableDataModel { protected final BtcWalletService btcWalletService; + protected final OfferUtil offerUtil; @Getter protected final BooleanProperty isBtcWalletFunded = new SimpleBooleanProperty(); @@ -54,8 +59,9 @@ public abstract class OfferDataModel extends ActivatableDataModel { protected AddressEntry addressEntry; protected boolean useSavingsWallet; - public OfferDataModel(BtcWalletService btcWalletService) { + public OfferDataModel(BtcWalletService btcWalletService, OfferUtil offerUtil) { this.btcWalletService = btcWalletService; + this.offerUtil = offerUtil; } protected void updateBalance() { @@ -64,28 +70,15 @@ protected void updateBalance() { Coin savingWalletBalance = btcWalletService.getSavingWalletBalance(); totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance); if (totalToPayAsCoin.get() != null) { - if (totalAvailableBalance.compareTo(totalToPayAsCoin.get()) > 0) - balance.set(totalToPayAsCoin.get()); - else - balance.set(totalAvailableBalance); + balance.set(minCoin(totalToPayAsCoin.get(), totalAvailableBalance)); } } else { balance.set(tradeWalletBalance); } - if (totalToPayAsCoin.get() != null) { - Coin missing = totalToPayAsCoin.get().subtract(balance.get()); - if (missing.isNegative()) - missing = Coin.ZERO; - missingCoin.set(missing); - } - - isBtcWalletFunded.set(isBalanceSufficient(balance.get())); + missingCoin.set(offerUtil.getBalanceShortage(totalToPayAsCoin.get(), balance.get())); + isBtcWalletFunded.set(offerUtil.isBalanceSufficient(totalToPayAsCoin.get(), balance.get())); if (totalToPayAsCoin.get() != null && isBtcWalletFunded.get() && !showWalletFundedNotification.get()) { showWalletFundedNotification.set(true); } } - - private boolean isBalanceSufficient(Coin balance) { - return totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0; - } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java index c0f5e822f24..0bec3dcfd8a 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java @@ -22,13 +22,13 @@ package bisq.desktop.main.offer.createoffer; import bisq.desktop.Navigation; -import bisq.desktop.main.offer.MakerFeeProvider; import bisq.desktop.main.offer.MutableOfferDataModel; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.CreateOfferService; +import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; @@ -54,6 +54,7 @@ class CreateOfferDataModel extends MutableOfferDataModel { @Inject public CreateOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, + OfferUtil offerUtil, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, Preferences preferences, @@ -63,11 +64,11 @@ public CreateOfferDataModel(CreateOfferService createOfferService, AccountAgeWitnessService accountAgeWitnessService, FeeService feeService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - MakerFeeProvider makerFeeProvider, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(createOfferService, openOfferManager, + offerUtil, btcWalletService, bsqWalletService, preferences, @@ -77,7 +78,6 @@ public CreateOfferDataModel(CreateOfferService createOfferService, accountAgeWitnessService, feeService, btcFormatter, - makerFeeProvider, tradeStatisticsManager, navigation); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java index 2543a002ef5..62fef3ac7ee 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java @@ -28,6 +28,7 @@ import bisq.desktop.util.validation.SecurityDepositValidator; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.offer.OfferUtil; import bisq.core.provider.price.PriceFeedService; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; @@ -53,7 +54,8 @@ public CreateOfferViewModel(CreateOfferDataModel dataModel, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - BsqFormatter bsqFormatter) { + BsqFormatter bsqFormatter, + OfferUtil offerUtil) { super(dataModel, fiatVolumeValidator, fiatPriceValidator, @@ -65,6 +67,8 @@ public CreateOfferViewModel(CreateOfferDataModel dataModel, accountAgeWitnessService, navigation, preferences, - btcFormatter, bsqFormatter); + btcFormatter, + bsqFormatter, + offerUtil); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 95cff8bc867..399f45bbef3 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -38,7 +38,6 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; -import bisq.core.payment.HalCashAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.payload.PaymentMethod; @@ -48,6 +47,7 @@ import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.user.Preferences; import bisq.core.user.User; +import bisq.core.util.VolumeUtil; import bisq.core.util.coin.CoinUtil; import bisq.network.p2p.P2PService; @@ -123,6 +123,7 @@ class TakeOfferDataModel extends OfferDataModel { @Inject TakeOfferDataModel(TradeManager tradeManager, OfferBook offerBook, + OfferUtil offerUtil, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, User user, FeeService feeService, @@ -134,7 +135,7 @@ class TakeOfferDataModel extends OfferDataModel { Navigation navigation, P2PService p2PService ) { - super(btcWalletService); + super(btcWalletService, offerUtil); this.tradeManager = tradeManager; this.offerBook = offerBook; @@ -463,9 +464,9 @@ void calculateVolume() { !amount.get().isZero()) { Volume volumeByAmount = tradePrice.getVolumeByAmount(amount.get()); if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) - volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount); + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(getCurrencyCode())) - volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount); + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); volume.set(volumeByAmount); @@ -643,11 +644,11 @@ public Coin getUsableBsqBalance() { } public boolean isHalCashAccount() { - return paymentAccount instanceof HalCashAccount; + return paymentAccount.isHalCashAccount(); } public boolean isCurrencyForTakerFeeBtc() { - return OfferUtil.isCurrencyForTakerFeeBtc(preferences, bsqWalletService, amount.get()); + return offerUtil.isCurrencyForTakerFeeBtc(amount.get()); } public void setPreferredCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) { @@ -659,18 +660,18 @@ public boolean isPreferredFeeCurrencyBtc() { } public Coin getTakerFeeInBtc() { - return OfferUtil.getTakerFee(true, amount.get()); + return offerUtil.getTakerFee(true, amount.get()); } public Coin getTakerFeeInBsq() { - return OfferUtil.getTakerFee(false, amount.get()); + return offerUtil.getTakerFee(false, amount.get()); } boolean isTakerFeeValid() { - return preferences.getPayFeeInBtc() || OfferUtil.isBsqForTakerFeeAvailable(bsqWalletService, amount.get()); + return preferences.getPayFeeInBtc() || offerUtil.isBsqForTakerFeeAvailable(amount.get()); } public boolean isBsqForFeeAvailable() { - return OfferUtil.isBsqForTakerFeeAvailable(bsqWalletService, amount.get()); + return offerUtil.isBsqForTakerFeeAvailable(amount.get()); } } 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 5496ca957c5..0bf3855eb24 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 @@ -41,12 +41,11 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.fee.FeeService; -import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.Trade; -import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.CoinUtil; import bisq.core.util.validation.InputValidator; import bisq.network.p2p.P2PService; @@ -86,11 +85,10 @@ class TakeOfferViewModel extends ActivatableWithDataModel implements ViewModel { final TakeOfferDataModel dataModel; + private final OfferUtil offerUtil; private final BtcValidator btcValidator; private final P2PService p2PService; - private final Preferences preferences; - private final PriceFeedService priceFeedService; - private AccountAgeWitnessService accountAgeWitnessService; + private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; @@ -101,7 +99,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private Trade trade; private Offer offer; private String price; - private String directionLabel; private String amountDescription; final StringProperty amount = new SimpleStringProperty(); @@ -146,21 +143,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel im @Inject public TakeOfferViewModel(TakeOfferDataModel dataModel, + OfferUtil offerUtil, BtcValidator btcValidator, P2PService p2PService, - Preferences preferences, - PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, BsqFormatter bsqFormatter) { super(dataModel); this.dataModel = dataModel; - + this.offerUtil = offerUtil; this.btcValidator = btcValidator; this.p2PService = p2PService; - this.preferences = preferences; - this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.btcFormatter = btcFormatter; @@ -207,13 +201,9 @@ void initWithData(Offer offer) { dataModel.initWithData(offer); this.offer = offer; - if (offer.isBuyOffer()) { - directionLabel = Res.get("shared.sellBitcoin"); - amountDescription = Res.get("takeOffer.amountPriceBox.buy.amountDescription"); - } else { - directionLabel = Res.get("shared.buyBitcoin"); - amountDescription = Res.get("takeOffer.amountPriceBox.sell.amountDescription"); - } + amountDescription = offer.isBuyOffer() + ? Res.get("takeOffer.amountPriceBox.buy.amountDescription") + : Res.get("takeOffer.amountPriceBox.sell.amountDescription"); amountRange = btcFormatter.formatCoin(offer.getMinAmount()) + " - " + btcFormatter.formatCoin(offer.getAmount()); price = FormattingUtils.formatPrice(dataModel.tradePrice); @@ -296,8 +286,9 @@ private void applyTakerFee() { tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin)); Coin makerFeeInBtc = dataModel.getTakerFeeInBtc(); - Optional optionalBtcFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBtc, - true, preferences, priceFeedService, bsqFormatter); + Optional optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc, + true, + bsqFormatter); String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter); if (DevEnv.isDaoActivated()) { tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount); @@ -306,8 +297,9 @@ private void applyTakerFee() { } Coin makerFeeInBsq = dataModel.getTakerFeeInBsq(); - Optional optionalBsqFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBsq, - false, preferences, priceFeedService, bsqFormatter); + Optional optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq, + false, + bsqFormatter); String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, optionalBsqFeeInFiat, bsqFormatter); if (DevEnv.isDaoActivated()) { tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount); @@ -355,7 +347,7 @@ void onFocusOutAmountTextField(boolean oldValue, boolean newValue, String userIn Price tradePrice = dataModel.tradePrice; long maxTradeLimit = dataModel.getMaxTradeLimit(); if (dataModel.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) { - Coin adjustedAmountForHalCash = OfferUtil.getAdjustedAmountForHalCash(dataModel.getAmount().get(), + Coin adjustedAmountForHalCash = CoinUtil.getAdjustedAmountForHalCash(dataModel.getAmount().get(), tradePrice, maxTradeLimit); dataModel.applyAmount(adjustedAmountForHalCash); @@ -364,7 +356,7 @@ void onFocusOutAmountTextField(boolean oldValue, boolean newValue, String userIn if (!isAmountEqualMinAmount(dataModel.getAmount().get()) && (!isAmountEqualMaxAmount(dataModel.getAmount().get()))) { // We only apply the rounding if the amount is variable (minAmount is lower as amount). // Otherwise we could get an amount lower then the minAmount set by rounding - Coin roundedAmount = OfferUtil.getRoundedFiatAmount(dataModel.getAmount().get(), tradePrice, + Coin roundedAmount = CoinUtil.getRoundedFiatAmount(dataModel.getAmount().get(), tradePrice, maxTradeLimit); dataModel.applyAmount(roundedAmount); } @@ -638,12 +630,12 @@ private void setAmountToModel() { Price price = dataModel.tradePrice; if (price != null) { if (dataModel.isHalCashAccount()) { - amount = OfferUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); + amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); } else if (CurrencyUtil.isFiatCurrency(dataModel.getCurrencyCode()) && !isAmountEqualMinAmount(amount) && !isAmountEqualMaxAmount(amount)) { // We only apply the rounding if the amount is variable (minAmount is lower as amount). // Otherwise we could get an amount lower then the minAmount set by rounding - amount = OfferUtil.getRoundedFiatAmount(amount, price, maxTradeLimit); + amount = CoinUtil.getRoundedFiatAmount(amount, price, maxTradeLimit); } } dataModel.applyAmount(amount); @@ -694,10 +686,6 @@ public String getPrice() { return price; } - public String getDirectionLabel() { - return directionLabel; - } - public String getAmountDescription() { return amountDescription; } @@ -757,10 +745,6 @@ public String getTxFeePercentage() { return GUIUtil.getPercentage(txFeeAsCoin, dataModel.getAmount().get()); } - public PaymentMethod getPaymentMethod() { - return dataModel.getPaymentMethod(); - } - ObservableList getPossiblePaymentAccounts() { return dataModel.getPossiblePaymentAccounts(); } @@ -781,14 +765,6 @@ public void resetErrorMessage() { offer.setErrorMessage(null); } - public String getBuyerSecurityDeposit() { - return btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit()); - } - - public String getSellerSecurityDeposit() { - return btcFormatter.formatCoin(dataModel.getSellerSecurityDeposit()); - } - private CoinFormatter getFormatterForTakerFee() { return dataModel.isCurrencyForTakerFeeBtc() ? btcFormatter : bsqFormatter; } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index f3ba118a13e..8956e028f5b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -19,7 +19,6 @@ import bisq.desktop.Navigation; -import bisq.desktop.main.offer.MakerFeeProvider; import bisq.desktop.main.offer.MutableOfferDataModel; import bisq.core.account.witness.AccountAgeWitnessService; @@ -31,6 +30,7 @@ import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; @@ -64,6 +64,7 @@ class EditOfferDataModel extends MutableOfferDataModel { @Inject EditOfferDataModel(CreateOfferService createOfferService, OpenOfferManager openOfferManager, + OfferUtil offerUtil, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, Preferences preferences, @@ -74,11 +75,12 @@ class EditOfferDataModel extends MutableOfferDataModel { FeeService feeService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, CorePersistenceProtoResolver corePersistenceProtoResolver, - MakerFeeProvider makerFeeProvider, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { + super(createOfferService, openOfferManager, + offerUtil, btcWalletService, bsqWalletService, preferences, @@ -88,7 +90,6 @@ class EditOfferDataModel extends MutableOfferDataModel { accountAgeWitnessService, feeService, btcFormatter, - makerFeeProvider, tradeStatisticsManager, navigation); this.corePersistenceProtoResolver = corePersistenceProtoResolver; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java index f85d1978b57..73b3c83781d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -27,6 +27,7 @@ import bisq.desktop.util.validation.SecurityDepositValidator; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOffer; import bisq.core.provider.price.PriceFeedService; import bisq.core.user.Preferences; @@ -56,7 +57,8 @@ public EditOfferViewModel(EditOfferDataModel dataModel, Navigation navigation, Preferences preferences, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - BsqFormatter bsqFormatter) { + BsqFormatter bsqFormatter, + OfferUtil offerUtil) { super(dataModel, fiatVolumeValidator, fiatPriceValidator, @@ -68,7 +70,9 @@ public EditOfferViewModel(EditOfferDataModel dataModel, accountAgeWitnessService, navigation, preferences, - btcFormatter, bsqFormatter); + btcFormatter, + bsqFormatter, + offerUtil); syncMinAmountWithAmount = false; } diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index e9550895894..e8daeb57dc8 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -1,7 +1,5 @@ package bisq.desktop.main.offer.createoffer; -import bisq.desktop.main.offer.MakerFeeProvider; - import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.CryptoCurrency; @@ -9,7 +7,7 @@ import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.offer.CreateOfferService; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferUtil; import bisq.core.payment.ClearXchangeAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.RevolutAccount; @@ -29,6 +27,7 @@ import org.junit.Before; import org.junit.Test; +import static bisq.core.offer.OfferPayload.Direction; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -40,7 +39,7 @@ public class CreateOfferDataModelTest { private CreateOfferDataModel model; private User user; private Preferences preferences; - private MakerFeeProvider makerFeeProvider; + private OfferUtil offerUtil; @Before public void setUp() { @@ -54,6 +53,7 @@ public void setUp() { FeeService feeService = mock(FeeService.class); CreateOfferService createOfferService = mock(CreateOfferService.class); preferences = mock(Preferences.class); + offerUtil = mock(OfferUtil.class); user = mock(User.class); var tradeStats = mock(TradeStatisticsManager.class); @@ -63,11 +63,20 @@ public void setUp() { when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); - makerFeeProvider = mock(MakerFeeProvider.class); - model = new CreateOfferDataModel(createOfferService, null, btcWalletService, - null, preferences, user, null, - priceFeedService, null, - feeService, null, makerFeeProvider, tradeStats, null); + model = new CreateOfferDataModel(createOfferService, + null, + offerUtil, + btcWalletService, + null, + preferences, + user, + null, + priceFeedService, + null, + feeService, + null, + tradeStats, + null); } @Test @@ -84,9 +93,9 @@ public void testUseTradeCurrencySetInOfferViewWhenInPaymentAccountAvailable() { when(user.getPaymentAccounts()).thenReturn(paymentAccounts); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); - when(makerFeeProvider.getMakerFee(any(), any(), any())).thenReturn(Coin.ZERO); + when(offerUtil.getMakerFee(any())).thenReturn(Coin.ZERO); - model.initWithData(OfferPayload.Direction.BUY, new FiatCurrency("USD")); + model.initWithData(Direction.BUY, new FiatCurrency("USD")); assertEquals("USD", model.getTradeCurrencyCode().get()); } @@ -104,10 +113,9 @@ public void testUseTradeAccountThatMatchesTradeCurrencySetInOffer() { when(user.getPaymentAccounts()).thenReturn(paymentAccounts); when(user.findFirstPaymentAccountWithCurrency(new FiatCurrency("USD"))).thenReturn(zelleAccount); when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); - when(makerFeeProvider.getMakerFee(any(), any(), any())).thenReturn(Coin.ZERO); + when(offerUtil.getMakerFee(any())).thenReturn(Coin.ZERO); - model.initWithData(OfferPayload.Direction.BUY, new FiatCurrency("USD")); + model.initWithData(Direction.BUY, new FiatCurrency("USD")); assertEquals("USD", model.getTradeCurrencyCode().get()); } - } diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 17d8ab802e9..f82c5636e04 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -17,7 +17,6 @@ package bisq.desktop.main.offer.createoffer; -import bisq.desktop.main.offer.MakerFeeProvider; import bisq.desktop.util.validation.AltcoinValidator; import bisq.desktop.util.validation.BtcValidator; import bisq.desktop.util.validation.FiatPriceValidator; @@ -32,7 +31,7 @@ import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.offer.CreateOfferService; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferUtil; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.fee.FeeService; @@ -61,6 +60,7 @@ import org.junit.Before; import org.junit.Test; +import static bisq.core.offer.OfferPayload.Direction; import static bisq.desktop.maker.PreferenceMakers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -73,7 +73,8 @@ public class CreateOfferViewModelTest { private CreateOfferViewModel model; - private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat()); + private final CoinFormatter coinFormatter = new ImmutableCoinFormatter( + Config.baseCurrencyNetworkParameters().getMonetaryFormat()); @Before public void setUp() { @@ -97,12 +98,17 @@ public void setUp() { SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class); AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class); CreateOfferService createOfferService = mock(CreateOfferService.class); + OfferUtil offerUtil = mock(OfferUtil.class); var tradeStats = mock(TradeStatisticsManager.class); when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(btcWalletService.getBalanceForAddress(any())).thenReturn(Coin.valueOf(1000L)); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); - when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); + when(priceFeedService.getMarketPrice(anyString())).thenReturn( + new MarketPrice("USD", + 12684.0450, + Instant.now().getEpochSecond(), + true)); when(feeService.getTxFee(anyInt())).thenReturn(Coin.valueOf(1000L)); when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount); when(paymentAccount.getPaymentMethod()).thenReturn(mock(PaymentMethod.class)); @@ -115,16 +121,37 @@ public void setUp() { when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); - CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, null, btcWalletService, - bsqWalletService, empty, user, null, priceFeedService, - accountAgeWitnessService, feeService, - coinFormatter, mock(MakerFeeProvider.class), tradeStats, null); - dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin")); + CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, + null, + offerUtil, + btcWalletService, + bsqWalletService, + empty, + user, + null, + priceFeedService, + accountAgeWitnessService, + feeService, + coinFormatter, + tradeStats, + null); + dataModel.initWithData(Direction.BUY, new CryptoCurrency("BTC", "bitcoin")); dataModel.activate(); - model = new CreateOfferViewModel(dataModel, null, fiatPriceValidator, altcoinValidator, - btcValidator, null, securityDepositValidator, priceFeedService, null, null, - preferences, coinFormatter, bsqFormatter); + model = new CreateOfferViewModel(dataModel, + null, + fiatPriceValidator, + altcoinValidator, + btcValidator, + null, + securityDepositValidator, + priceFeedService, + null, + null, + preferences, + coinFormatter, + bsqFormatter, + offerUtil); model.activate(); } diff --git a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java index a820789916d..d0b4b01cac6 100644 --- a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java @@ -1,6 +1,5 @@ package bisq.desktop.main.portfolio.editoffer; -import bisq.desktop.main.offer.MakerFeeProvider; import bisq.desktop.util.validation.SecurityDepositValidator; import bisq.core.account.witness.AccountAgeWitnessService; @@ -13,6 +12,7 @@ import bisq.core.locale.Res; import bisq.core.offer.CreateOfferService; import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOffer; import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.payment.PaymentAccount; @@ -77,11 +77,16 @@ public void setUp() { SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class); AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class); CreateOfferService createOfferService = mock(CreateOfferService.class); + OfferUtil offerUtil = mock(OfferUtil.class); when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry); when(btcWalletService.getBalanceForAddress(any())).thenReturn(Coin.valueOf(1000L)); when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty()); - when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); + when(priceFeedService.getMarketPrice(anyString())).thenReturn( + new MarketPrice("USD", + 12684.0450, + Instant.now().getEpochSecond(), + true)); when(feeService.getTxFee(anyInt())).thenReturn(Coin.valueOf(1000L)); when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); @@ -92,11 +97,21 @@ public void setUp() { when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO); when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); - model = new EditOfferDataModel(createOfferService, null, - btcWalletService, bsqWalletService, empty, user, - null, priceFeedService, - accountAgeWitnessService, feeService, null, null, - mock(MakerFeeProvider.class), mock(TradeStatisticsManager.class), null); + model = new EditOfferDataModel(createOfferService, + null, + offerUtil, + btcWalletService, + bsqWalletService, + empty, + user, + null, + priceFeedService, + accountAgeWitnessService, + feeService, + null, + null, + mock(TradeStatisticsManager.class), + null); } @Test