From ab6be235162764d3a3af0a3ad4b45a4ca3864848 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 20 Oct 2020 15:06:44 -0300 Subject: [PATCH] Refactor offer/trade related classes in core and desktop These refactoring changes are for reducing existing and potential duplication coming with the addition of new trading protocol support in the gRPC API. Some minor styling and logic simplification changes are also include. - Convert OfferUtil to injected singleton, and move various offer related utility methods into it. - Delete both MakerFeeProvider classes, which were wrappers around the same static old OfferUtil method. - Inject OfferUtil into CreateOfferDataModel, CreateOfferViewModel, TakeOfferDataModel, TakeOfferViewModel, MutableOfferDataModel, MutableOfferViewModel, OfferDataModel, EditOfferDataModel, EditOfferViewModel - Refactor TakeOfferViewModel Use OfferUtil, remove unused fields & methods. Made minor logic simplification, style and formatting changes. - MutableOfferDataModel Made minor logic simplification, style and formatting changes. - MutableOfferView uses new paymentAccount.isHalCashAccount(). - MutableOfferViewModel Refactored to use new VolumeUtil, CoinUtil, OfferUtil. Removed unused fields & accessors. Made minor style change. - Refactored OfferDataModel to use new OfferUtil - Refactor CreateOfferService Inject and use OfferUtil Move some utility methods to OfferUtil Remove unused fields - Offer Refactored to use new VolumeUtil for volume calculations. Made stateProperty and errorMessageProperty fields private. - PaymentAccount Moved isHalCashAccount type check to this class. Moved getTradeCurrency logic to this class. - Contract, radeStatistics2, TradeStatistics3 Refactored to use new VolumeUtil for volume calculations. - Trade Refactored to use new VolumeUtil for volume calculations. Made minor logic simplification, style and formatting changes. - CoinUtil Moved some coin utility methods into this class - CoinUtilTest Moved (coin related) tests from CoinCryptoUtilsTest and OfferUtilTest into CoinUtilTest, and deleted OfferUtilTest, CoinCryptoUtilsTest. - Adjust create and edit offer tests to model refactoring --- .../bisq/core/offer/CreateOfferService.java | 84 +--- .../bisq/core/offer/MakerFeeProvider.java | 29 -- core/src/main/java/bisq/core/offer/Offer.java | 9 +- .../main/java/bisq/core/offer/OfferUtil.java | 408 ++++++++---------- .../bisq/core/payment/PaymentAccount.java | 27 +- .../main/java/bisq/core/trade/Contract.java | 6 +- core/src/main/java/bisq/core/trade/Trade.java | 34 +- .../trade/statistics/TradeStatistics2.java | 4 +- .../trade/statistics/TradeStatistics3.java | 4 +- .../main/java/bisq/core/util/VolumeUtil.java | 50 +++ .../java/bisq/core/util/coin/CoinUtil.java | 109 +++++ .../bisq/core/util/CoinCryptoUtilsTest.java | 60 --- .../coin/CoinUtilTest.java} | 56 ++- .../desktop/main/offer/MakerFeeProvider.java | 13 - .../main/offer/MutableOfferDataModel.java | 88 ++-- .../desktop/main/offer/MutableOfferView.java | 2 +- .../main/offer/MutableOfferViewModel.java | 61 ++- .../desktop/main/offer/OfferDataModel.java | 31 +- .../createoffer/CreateOfferDataModel.java | 6 +- .../createoffer/CreateOfferViewModel.java | 8 +- .../offer/takeoffer/TakeOfferDataModel.java | 21 +- .../offer/takeoffer/TakeOfferViewModel.java | 60 +-- .../editoffer/EditOfferDataModel.java | 7 +- .../editoffer/EditOfferViewModel.java | 8 +- .../createoffer/CreateOfferDataModelTest.java | 36 +- .../createoffer/CreateOfferViewModelTest.java | 51 ++- .../editoffer/EditOfferDataModelTest.java | 29 +- 27 files changed, 656 insertions(+), 645 deletions(-) delete mode 100644 core/src/main/java/bisq/core/offer/MakerFeeProvider.java create mode 100644 core/src/main/java/bisq/core/util/VolumeUtil.java delete mode 100644 core/src/test/java/bisq/core/util/CoinCryptoUtilsTest.java rename core/src/test/java/bisq/core/{offer/OfferUtilTest.java => util/coin/CoinUtilTest.java} (57%) delete mode 100644 desktop/src/main/java/bisq/desktop/main/offer/MakerFeeProvider.java 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