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 1/9] 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 From e809af37ccea81b89b832d87be37827c00ac0a02 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 20 Oct 2020 16:00:05 -0300 Subject: [PATCH 2/9] Add 'takeoffer' API method - Add new core.offer.takeoffer.TakeOfferModel Would have been nice to move more logic from bisq.desktop.main.offer.takeoffer.TakeOfferDataModel, but it has JFX dependencies that cannot be use in :core. - Add grpc protos to support takeoffer, confirmpaymentsent, confirmpaymentreceived Only takeoffer is implemented in this commit. - Refactor OfferInfo grpc proto wrapper, and add offer state field - Add new TradeInfo grpc proto wrapper - Implement takeoffer on server and cli side - Refactor offer/trade tests, add test cases --- .../test/java/bisq/apitest/ApiTestCase.java | 7 +- .../java/bisq/apitest/method/MethodTest.java | 36 +++ ...eOfferTest.java => AbstractOfferTest.java} | 65 +++- .../offer/CreateOfferUsingFixedPriceTest.java | 2 +- ...CreateOfferUsingMarketPriceMarginTest.java | 2 +- .../method/offer/ValidateCreateOfferTest.java | 2 +- .../method/trade/AbstractTradeTest.java | 32 ++ .../method/trade/TakeBuyBTCOfferTest.java | 132 ++++++++ .../method/trade/TakeSellBTCOfferTest.java | 134 ++++++++ cli/src/main/java/bisq/cli/CliMain.java | 20 +- cli/src/main/java/bisq/cli/GrpcStubs.java | 3 + core/src/main/java/bisq/core/api/CoreApi.java | 23 +- .../java/bisq/core/api/CoreTradesService.java | 91 ++++++ .../java/bisq/core/api/model/OfferInfo.java | 44 ++- .../java/bisq/core/api/model/TradeInfo.java | 211 +++++++++++++ .../core/offer/takeoffer/TakeOfferModel.java | 293 ++++++++++++++++++ .../bisq/daemon/grpc/GrpcOffersService.java | 29 +- .../java/bisq/daemon/grpc/GrpcServer.java | 2 + .../bisq/daemon/grpc/GrpcTradesService.java | 87 ++++++ proto/src/main/proto/grpc.proto | 64 +++- 20 files changed, 1231 insertions(+), 48 deletions(-) rename apitest/src/test/java/bisq/apitest/method/offer/{AbstractCreateOfferTest.java => AbstractOfferTest.java} (55%) create mode 100644 apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java create mode 100644 apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java create mode 100644 apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java create mode 100644 core/src/main/java/bisq/core/api/CoreTradesService.java create mode 100644 core/src/main/java/bisq/core/api/model/TradeInfo.java create mode 100644 core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java create mode 100644 daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java diff --git a/apitest/src/test/java/bisq/apitest/ApiTestCase.java b/apitest/src/test/java/bisq/apitest/ApiTestCase.java index 144df9d3e99..854ce4c59bc 100644 --- a/apitest/src/test/java/bisq/apitest/ApiTestCase.java +++ b/apitest/src/test/java/bisq/apitest/ApiTestCase.java @@ -105,7 +105,12 @@ protected static GrpcStubs grpcStubs(BisqAppConfig bisqAppConfig) { } } - protected void sleep(long ms) { + protected static void genBtcBlocksThenWait(int numBlocks, long wait) { + bitcoinCli.generateBlocks(numBlocks); + sleep(wait); + } + + protected static void sleep(long ms) { try { MILLISECONDS.sleep(ms); } catch (InterruptedException ignored) { diff --git a/apitest/src/test/java/bisq/apitest/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index b03f00da356..6a175d118ce 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,17 +17,22 @@ package bisq.apitest.method; +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetOfferRequest; import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.UnlockWalletRequest; import protobuf.PaymentAccount; @@ -88,6 +93,22 @@ protected final GetOfferRequest createGetOfferRequest(String offerId) { return GetOfferRequest.newBuilder().setId(offerId).build(); } + protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) { + return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build(); + } + + protected final GetTradeRequest createGetTradeRequest(String tradeId) { + return GetTradeRequest.newBuilder().setTradeId(tradeId).build(); + } + + protected final ConfirmPaymentStartedRequest createConfirmPaymentStartedRequest(String tradeId) { + return ConfirmPaymentStartedRequest.newBuilder().setTradeId(tradeId).build(); + } + + protected final ConfirmPaymentReceivedRequest createConfirmPaymentReceivedRequest(String tradeId) { + return ConfirmPaymentReceivedRequest.newBuilder().setTradeId(tradeId).build(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. protected final long getBalance(BisqAppConfig bisqAppConfig) { @@ -149,6 +170,21 @@ protected final OfferInfo getOffer(BisqAppConfig bisqAppConfig, String offerId) return grpcStubs(bisqAppConfig).offersService.getOffer(req).getOffer(); } + protected final TradeInfo getTrade(BisqAppConfig bisqAppConfig, String tradeId) { + var req = createGetTradeRequest(tradeId); + return grpcStubs(bisqAppConfig).tradesService.getTrade(req).getTrade(); + } + + protected final void confirmPaymentStarted(BisqAppConfig bisqAppConfig, String tradeId) { + var req = createConfirmPaymentStartedRequest(tradeId); + grpcStubs(bisqAppConfig).tradesService.confirmPaymentStarted(req); + } + + protected final void confirmPaymentReceived(BisqAppConfig bisqAppConfig, String tradeId) { + var req = createConfirmPaymentReceivedRequest(tradeId); + grpcStubs(bisqAppConfig).tradesService.confirmPaymentReceived(req); + } + // Static conveniences for test methods and test case fixture setups. protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) { diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractCreateOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java similarity index 55% rename from apitest/src/test/java/bisq/apitest/method/offer/AbstractCreateOfferTest.java rename to apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index 9c494a7a74d..979d7a33e6a 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractCreateOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -19,9 +19,12 @@ import bisq.core.monetary.Altcoin; +import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.OfferInfo; +import protobuf.PaymentAccount; + import org.bitcoinj.utils.Fiat; import java.math.BigDecimal; @@ -36,14 +39,16 @@ import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind; import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.arbdaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.apitest.config.BisqAppConfig.seednode; import static bisq.common.util.MathUtils.roundDouble; import static bisq.common.util.MathUtils.scaleDownByPowerOf10; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static java.lang.String.format; import static java.math.RoundingMode.HALF_UP; import static java.util.Comparator.comparing; -import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.junit.jupiter.api.Assertions.fail; @@ -51,11 +56,11 @@ import bisq.apitest.method.MethodTest; import bisq.cli.GrpcStubs; - @Slf4j -abstract class AbstractCreateOfferTest extends MethodTest { +public abstract class AbstractOfferTest extends MethodTest { protected static GrpcStubs aliceStubs; + protected static GrpcStubs bobStubs; @BeforeAll public static void setUp() { @@ -64,36 +69,74 @@ public static void setUp() { static void startSupportingApps() { try { - // setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,alicedaemon", "--enableBisqDebugging", "true"}); - setUpScaffold(bitcoind, seednode, alicedaemon); + // setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon", "--enableBisqDebugging", "true"}); + setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon, bobdaemon); + registerDisputeAgents(arbdaemon); aliceStubs = grpcStubs(alicedaemon); + bobStubs = grpcStubs(bobdaemon); // Generate 1 regtest block for alice's wallet to show 10 BTC balance, // and give alicedaemon time to parse the new block. - bitcoinCli.generateBlocks(1); - MILLISECONDS.sleep(1500); + genBtcBlocksThenWait(1, 1500); } catch (Exception ex) { fail(ex); } } + protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount, + String direction, + String currencyCode, + long amount) { + return createMarketBasedPricedOffer(aliceStubs, paymentAccount, direction, currencyCode, amount); + } + + protected final OfferInfo createBobOffer(PaymentAccount paymentAccount, + String direction, + String currencyCode, + long amount) { + return createMarketBasedPricedOffer(bobStubs, paymentAccount, direction, currencyCode, amount); + } + + protected final OfferInfo createMarketBasedPricedOffer(GrpcStubs grpcStubs, + PaymentAccount paymentAccount, + String direction, + String currencyCode, + long amount) { + var req = CreateOfferRequest.newBuilder() + .setPaymentAccountId(paymentAccount.getId()) + .setDirection(direction) + .setCurrencyCode(currencyCode) + .setAmount(amount) + .setMinAmount(amount) + .setUseMarketBasedPrice(true) + .setMarketPriceMargin(0.00) + .setPrice("0") + .setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent()) + .build(); + return grpcStubs.offersService.createOffer(req).getOffer(); + } + protected final OfferInfo getOffer(String offerId) { return aliceStubs.offersService.getOffer(createGetOfferRequest(offerId)).getOffer(); } - protected final OfferInfo getMostRecentOffer(String direction, String currencyCode) { - List offerInfoList = getOffersSortedByDate(direction, currencyCode); + protected final OfferInfo getMostRecentOffer(GrpcStubs grpcStubs, String direction, String currencyCode) { + List offerInfoList = getOffersSortedByDate(grpcStubs, direction, currencyCode); if (offerInfoList.isEmpty()) fail(format("No %s offers found for currency %s", direction, currencyCode)); return offerInfoList.get(offerInfoList.size() - 1); } - protected final List getOffersSortedByDate(String direction, String currencyCode) { + protected final int getOpenOffersCount(GrpcStubs grpcStubs, String direction, String currencyCode) { + return getOffersSortedByDate(grpcStubs, direction, currencyCode).size(); + } + + protected final List getOffersSortedByDate(GrpcStubs grpcStubs, String direction, String currencyCode) { var req = GetOffersRequest.newBuilder() .setDirection(direction) .setCurrencyCode(currencyCode).build(); - var reply = aliceStubs.offersService.getOffers(req); + var reply = grpcStubs.offersService.getOffers(req); return sortOffersByDate(reply.getOffersList()); } diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 39606536558..739faf71e96 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -35,7 +35,7 @@ @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest { +public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest { @Test @Order(1) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index 331ddbab736..f9d379131bb 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -43,7 +43,7 @@ @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTest { +public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { private static final DecimalFormat PCT_FORMAT = new DecimalFormat("##0.00"); private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50% diff --git a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java index c51b64a6a0f..785dc97fdcb 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/ValidateCreateOfferTest.java @@ -36,7 +36,7 @@ @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class ValidateCreateOfferTest extends AbstractCreateOfferTest { +public class ValidateCreateOfferTest extends AbstractOfferTest { @Test @Order(1) diff --git a/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java new file mode 100644 index 00000000000..5364de7f7d8 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/AbstractTradeTest.java @@ -0,0 +1,32 @@ +package bisq.apitest.method.trade; + +import bisq.core.trade.Trade; + +import bisq.proto.grpc.TradeInfo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + + +import bisq.apitest.method.offer.AbstractOfferTest; + +public class AbstractTradeTest extends AbstractOfferTest { + + protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId) { + return bobStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade(); + } + + protected final TradeInfo takeBobsOffer(String offerId, String paymentAccountId) { + return aliceStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade(); + } + + protected final void verifyExpectedTradeStateAndPhase(TradeInfo trade, + Trade.State expectedState, + Trade.Phase expectedPhase) { + assertNotNull(trade); + assertEquals(expectedState.name(), trade.getState()); + assertEquals(expectedPhase.name(), trade.getPhase()); + } + +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java new file mode 100644 index 00000000000..ac46307ce1c --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -0,0 +1,132 @@ +/* + * 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.apitest.method.trade; + +import protobuf.PaymentAccount; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG; +import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; +import static bisq.core.trade.Trade.State.SELLER_PUBLISHED_DEPOSIT_TX; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.Offer.State.OFFER_FEE_PAID; +import static protobuf.OpenOffer.State.AVAILABLE; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeBuyBTCOfferTest extends AbstractTradeTest { + + // Alice is buyer, Bob is seller. + + private static String tradeId; + + private PaymentAccount alicesAccount; + private PaymentAccount bobsAccount; + + @BeforeEach + public void init() { + alicesAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); + bobsAccount = getDefaultPerfectDummyPaymentAccount(bobdaemon); + } + + @Test + @Order(1) + public void testTakeAlicesBuyOffer() { + try { + var alicesOffer = createAliceOffer(alicesAccount, "buy", "usd", 12500000); + var offerId = alicesOffer.getId(); + + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay. + sleep(3000); + assertEquals(1, getOpenOffersCount(aliceStubs, "buy", "usd")); + + var trade = takeAlicesOffer(offerId, bobsAccount.getId()); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + genBtcBlocksThenWait(1, 2250); + assertEquals(0, getOpenOffersCount(aliceStubs, "buy", "usd")); + + trade = getTrade(bobdaemon, trade.getTradeId()); + verifyExpectedTradeStateAndPhase(trade, SELLER_PUBLISHED_DEPOSIT_TX, DEPOSIT_PUBLISHED); + + genBtcBlocksThenWait(1, 2250); + trade = getTrade(bobdaemon, trade.getTradeId()); + verifyExpectedTradeStateAndPhase(trade, DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, DEPOSIT_CONFIRMED); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Disabled + @Test + @Order(2) + public void testAlicesConfirmPaymentStarted() { + try { + var trade = getTrade(alicedaemon, tradeId); + assertNotNull(trade); + + confirmPaymentStarted(alicedaemon, trade.getTradeId()); + sleep(3000); + + trade = getTrade(alicedaemon, tradeId); + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); + verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Disabled + @Test + @Order(3) + public void testBobsConfirmPaymentReceived() { + var trade = getTrade(bobdaemon, tradeId); + assertNotNull(trade); + + confirmPaymentReceived(bobdaemon, trade.getTradeId()); + sleep(3000); + + trade = getTrade(bobdaemon, tradeId); + // TODO is this a bug? Why is offer.state == available? + assertEquals(AVAILABLE.name(), trade.getOffer().getState()); + verifyExpectedTradeStateAndPhase(trade, SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_PUBLISHED); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java new file mode 100644 index 00000000000..c3b9a918be0 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -0,0 +1,134 @@ +/* + * 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.apitest.method.trade; + +import protobuf.PaymentAccount; + +import io.grpc.StatusRuntimeException; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.BisqAppConfig.alicedaemon; +import static bisq.apitest.config.BisqAppConfig.bobdaemon; +import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED; +import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED; +import static bisq.core.trade.Trade.Phase.FIAT_SENT; +import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; +import static bisq.core.trade.Trade.State.BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG; +import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG; +import static bisq.core.trade.Trade.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; +import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.Offer.State.OFFER_FEE_PAID; +import static protobuf.OpenOffer.State.AVAILABLE; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeSellBTCOfferTest extends AbstractTradeTest { + + // Alice is seller, Bob is buyer. + + private static String tradeId; + + private PaymentAccount alicesAccount; + private PaymentAccount bobsAccount; + + @BeforeEach + public void init() { + alicesAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); + bobsAccount = getDefaultPerfectDummyPaymentAccount(bobdaemon); + } + + @Test + @Order(1) + public void testTakeAlicesSellOffer() { + try { + var alicesOffer = createAliceOffer(alicesAccount, "sell", "usd", 12500000); + var offerId = alicesOffer.getId(); + + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay, but taking sell offers + // seems to require more time to prepare. + sleep(3000); + assertEquals(1, getOpenOffersCount(bobStubs, "sell", "usd")); + + var trade = takeAlicesOffer(offerId, bobsAccount.getId()); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + // Cache the trade id for the other tests. + tradeId = trade.getTradeId(); + + genBtcBlocksThenWait(1, 4000); + assertEquals(0, getOpenOffersCount(bobStubs, "sell", "usd")); + + trade = getTrade(bobdaemon, trade.getTradeId()); + verifyExpectedTradeStateAndPhase(trade, BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG, DEPOSIT_PUBLISHED); + + genBtcBlocksThenWait(1, 2250); + trade = getTrade(bobdaemon, trade.getTradeId()); + verifyExpectedTradeStateAndPhase(trade, DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN, DEPOSIT_CONFIRMED); + + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Disabled + @Test + @Order(2) + public void testBobsConfirmPaymentStarted() { + try { + var trade = getTrade(bobdaemon, tradeId); + assertNotNull(trade); + + confirmPaymentStarted(bobdaemon, trade.getTradeId()); + sleep(3000); + + trade = getTrade(bobdaemon, tradeId); + // TODO is this a bug? Why is offer.state == available? + assertEquals(AVAILABLE.name(), trade.getOffer().getState()); + verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); + } catch (StatusRuntimeException e) { + fail(e); + } + } + + @Disabled + @Test + @Order(3) + public void testAlicesConfirmPaymentReceived() { + var trade = getTrade(alicedaemon, tradeId); + assertNotNull(trade); + + confirmPaymentReceived(alicedaemon, trade.getTradeId()); + sleep(3000); + + trade = getTrade(alicedaemon, tradeId); + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); + verifyExpectedTradeStateAndPhase(trade, SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG, PAYOUT_PUBLISHED); + } +} diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 9b73e560283..c459c536cd4 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -30,6 +30,7 @@ import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.UnlockWalletRequest; import io.grpc.StatusRuntimeException; @@ -70,6 +71,7 @@ private enum Method { createoffer, getoffer, getoffers, + takeoffer, createpaymentacct, getpaymentaccts, getversion, @@ -154,9 +156,10 @@ public static void run(String[] args) { GrpcStubs grpcStubs = new GrpcStubs(host, port, password); var disputeAgentsService = grpcStubs.disputeAgentsService; - var versionService = grpcStubs.versionService; var offersService = grpcStubs.offersService; var paymentAccountsService = grpcStubs.paymentAccountsService; + var tradesService = grpcStubs.tradesService; + var versionService = grpcStubs.versionService; var walletsService = grpcStubs.walletsService; try { @@ -254,6 +257,20 @@ public static void run(String[] args) { out.println(formatOfferTable(reply.getOffersList(), currencyCode)); return; } + case takeoffer: { + if (nonOptionArgs.size() < 3) + throw new IllegalArgumentException("incorrect parameter count, expecting offer id, payment acct id"); + + var offerId = nonOptionArgs.get(1); + var paymentAccountId = nonOptionArgs.get(2); + var request = TakeOfferRequest.newBuilder() + .setOfferId(offerId) + .setPaymentAccountId(paymentAccountId) + .build(); + var reply = tradesService.takeOffer(request); + out.printf("trade '%s' successfully taken", reply.getTrade().getShortId()); + return; + } case createpaymentacct: { if (nonOptionArgs.size() < 5) throw new IllegalArgumentException( @@ -381,6 +398,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "", "security deposit (%)", ""); stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id"); stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); + stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); stream.format(rowFormat, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts"); stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet"); diff --git a/cli/src/main/java/bisq/cli/GrpcStubs.java b/cli/src/main/java/bisq/cli/GrpcStubs.java index 0e5835a278c..2db33fcbaa9 100644 --- a/cli/src/main/java/bisq/cli/GrpcStubs.java +++ b/cli/src/main/java/bisq/cli/GrpcStubs.java @@ -22,6 +22,7 @@ import bisq.proto.grpc.OffersGrpc; import bisq.proto.grpc.PaymentAccountsGrpc; import bisq.proto.grpc.PriceGrpc; +import bisq.proto.grpc.TradesGrpc; import bisq.proto.grpc.WalletsGrpc; import io.grpc.CallCredentials; @@ -36,6 +37,7 @@ public class GrpcStubs { public final OffersGrpc.OffersBlockingStub offersService; public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService; public final PriceGrpc.PriceBlockingStub priceService; + public final TradesGrpc.TradesBlockingStub tradesService; public final WalletsGrpc.WalletsBlockingStub walletsService; public GrpcStubs(String apiHost, int apiPort, String apiPassword) { @@ -55,6 +57,7 @@ public GrpcStubs(String apiHost, int apiPort, String apiPassword) { this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials); + this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 08cc67c49dc..88e08bf6df5 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -22,6 +22,7 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.payment.PaymentAccount; +import bisq.core.trade.Trade; import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -51,6 +52,7 @@ public class CoreApi { private final CoreOffersService coreOffersService; private final CorePaymentAccountsService paymentAccountsService; private final CorePriceService corePriceService; + private final CoreTradesService coreTradesService; private final CoreWalletsService walletsService; private final TradeStatisticsManager tradeStatisticsManager; @@ -59,12 +61,14 @@ public CoreApi(CoreDisputeAgentsService coreDisputeAgentsService, CoreOffersService coreOffersService, CorePaymentAccountsService paymentAccountsService, CorePriceService corePriceService, + CoreTradesService coreTradesService, CoreWalletsService walletsService, TradeStatisticsManager tradeStatisticsManager) { this.coreDisputeAgentsService = coreDisputeAgentsService; this.coreOffersService = coreOffersService; - this.corePriceService = corePriceService; this.paymentAccountsService = paymentAccountsService; + this.coreTradesService = coreTradesService; + this.corePriceService = corePriceService; this.walletsService = walletsService; this.tradeStatisticsManager = tradeStatisticsManager; } @@ -164,6 +168,23 @@ public double getMarketPrice(String currencyCode) { return corePriceService.getMarketPrice(currencyCode); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Trades + /////////////////////////////////////////////////////////////////////////////////////////// + + public void takeOffer(String offerId, + String paymentAccountId, + Consumer resultHandler) { + Offer offer = coreOffersService.getOffer(offerId); + coreTradesService.takeOffer(offer, + paymentAccountId, + resultHandler); + } + + public Trade getTrade(String tradeId) { + return coreTradesService.getTrade(tradeId); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java new file mode 100644 index 00000000000..731bccebdb4 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -0,0 +1,91 @@ +/* + * 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.api; + +import bisq.core.offer.Offer; +import bisq.core.offer.takeoffer.TakeOfferModel; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.user.User; + +import javax.inject.Inject; + +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; + +@Slf4j +class CoreTradesService { + + private final TakeOfferModel takeOfferModel; + private final TradeManager tradeManager; + private final User user; + + @Inject + public CoreTradesService(TakeOfferModel takeOfferModel, + TradeManager tradeManager, + User user) { + this.takeOfferModel = takeOfferModel; + this.tradeManager = tradeManager; + this.user = user; + } + + void takeOffer(Offer offer, + String paymentAccountId, + Consumer resultHandler) { + var paymentAccount = user.getPaymentAccount(paymentAccountId); + if (paymentAccount == null) + throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId)); + + var useSavingsWallet = true; + takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet); + log.info("Initiating take {} offer, {}", + offer.isBuyOffer() ? "buy" : "sell", + takeOfferModel); + //noinspection ConstantConditions + tradeManager.onTakeOffer(offer.getAmount(), + takeOfferModel.getTxFeeFromFeeService(), + takeOfferModel.getTakerFee(), + takeOfferModel.isCurrencyForTakerFeeBtc(), + offer.getPrice().getValue(), + takeOfferModel.getFundsNeededForTrade(), + offer, + paymentAccountId, + useSavingsWallet, + trade -> { + resultHandler.accept(trade); + }, + errorMessage -> { + log.error(errorMessage); + throw new IllegalStateException(errorMessage); + } + ); + } + + + Trade getTrade(String tradeId) { + return getTradeWithId(tradeId); + } + + private Trade getTradeWithId(String tradeId) { + return tradeManager.getTradeById(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + } +} diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index fa6f0c95ea6..ad2389e438a 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -17,8 +17,12 @@ package bisq.core.api.model; +import bisq.core.offer.Offer; + import bisq.common.Payload; +import java.util.Objects; + import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -28,6 +32,10 @@ @Getter public class OfferInfo implements Payload { + // The client cannot see bisq.core.Offer or its fromProto method. We use the lighter + // weight OfferInfo proto wrapper instead, containing just enough fields to view, + // create and take offers. + private final String id; private final String direction; private final long price; @@ -46,6 +54,7 @@ public class OfferInfo implements Payload { private final String baseCurrencyCode; private final String counterCurrencyCode; private final long date; + private final String state; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.id; @@ -64,6 +73,29 @@ public OfferInfo(OfferInfoBuilder builder) { this.baseCurrencyCode = builder.baseCurrencyCode; this.counterCurrencyCode = builder.counterCurrencyCode; this.date = builder.date; + this.state = builder.state; + } + + public static OfferInfo toOfferInfo(Offer offer) { + return new OfferInfo.OfferInfoBuilder() + .withId(offer.getId()) + .withDirection(offer.getDirection().name()) + .withPrice(Objects.requireNonNull(offer.getPrice()).getValue()) + .withUseMarketBasedPrice(offer.isUseMarketBasedPrice()) + .withMarketPriceMargin(offer.getMarketPriceMargin()) + .withAmount(offer.getAmount().value) + .withMinAmount(offer.getMinAmount().value) + .withVolume(Objects.requireNonNull(offer.getVolume()).getValue()) + .withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue()) + .withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value) + .withPaymentAccountId(offer.getMakerPaymentAccountId()) + .withPaymentMethodId(offer.getPaymentMethod().getId()) + .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) + .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) + .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) + .withDate(offer.getDate().getTime()) + .withState(offer.getState().name()) + .build(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -89,12 +121,13 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { .setBaseCurrencyCode(baseCurrencyCode) .setCounterCurrencyCode(counterCurrencyCode) .setDate(date) + .setState(state) .build(); } public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { /* - TODO (will be needed by the createoffer method) + TODO? return new OfferInfo(proto.getOfferPayload().getId(), proto.getOfferPayload().getDate()); */ @@ -124,9 +157,7 @@ public static class OfferInfoBuilder { private String baseCurrencyCode; private String counterCurrencyCode; private long date; - - public OfferInfoBuilder() { - } + private String state; public OfferInfoBuilder withId(String id) { this.id = id; @@ -208,6 +239,11 @@ public OfferInfoBuilder withDate(long date) { return this; } + public OfferInfoBuilder withState(String state) { + this.state = state; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java new file mode 100644 index 00000000000..33b8059f9b5 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -0,0 +1,211 @@ +/* + * 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.api.model; + +import bisq.core.trade.Trade; + +import bisq.common.Payload; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import static bisq.core.api.model.OfferInfo.toOfferInfo; + +@EqualsAndHashCode +@Getter +public class TradeInfo implements Payload { + + // The client cannot see bisq.core.trade.Trade or its fromProto method. We use the + // lighter weight TradeInfo proto wrapper instead, containing just enough fields to + // view and interact with trades. + + private final OfferInfo offer; + private final String tradeId; + private final String shortId; + private final String state; + private final String phase; + private final String tradePeriodState; + private final boolean isDepositPublished; + private final boolean isDepositConfirmed; + private final boolean isFiatSent; + private final boolean isFiatReceived; + private final boolean isPayoutPublished; + private final boolean isWithdrawn; + + public TradeInfo(TradeInfoBuilder builder) { + this.offer = builder.offer; + this.tradeId = builder.tradeId; + this.shortId = builder.shortId; + this.state = builder.state; + this.phase = builder.phase; + this.tradePeriodState = builder.tradePeriodState; + this.isDepositPublished = builder.isDepositPublished; + this.isDepositConfirmed = builder.isDepositConfirmed; + this.isFiatSent = builder.isFiatSent; + this.isFiatReceived = builder.isFiatReceived; + this.isPayoutPublished = builder.isPayoutPublished; + this.isWithdrawn = builder.isWithdrawn; + } + + public static TradeInfo toTradeInfo(Trade trade) { + return new TradeInfo.TradeInfoBuilder() + .withOffer(toOfferInfo(trade.getOffer())) + .withTradeId(trade.getId()) + .withShortId(trade.getShortId()) + .withState(trade.getState().name()) + .withPhase(trade.getPhase().name()) + .withTradePeriodState(trade.getTradePeriodState().name()) + .withIsDepositPublished(trade.isDepositPublished()) + .withIsDepositConfirmed(trade.isDepositConfirmed()) + .withIsFiatSent(trade.isFiatSent()) + .withIsFiatReceived(trade.isFiatReceived()) + .withIsPayoutPublished(trade.isPayoutPublished()) + .withIsWithdrawn(trade.isWithdrawn()) + .build(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public bisq.proto.grpc.TradeInfo toProtoMessage() { + return bisq.proto.grpc.TradeInfo.newBuilder() + .setOffer(offer.toProtoMessage()) + .setTradeId(tradeId) + .setShortId(shortId) + .setState(state) + .setPhase(phase) + .setTradePeriodState(tradePeriodState) + .setIsDepositPublished(isDepositPublished) + .setIsDepositConfirmed(isDepositConfirmed) + .setIsFiatSent(isFiatSent) + .setIsFiatReceived(isFiatReceived) + .setIsPayoutPublished(isPayoutPublished) + .setIsWithdrawn(isWithdrawn) + .build(); + } + + public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) { + // TODO + return null; + } + + /* + * TradeInfoBuilder helps avoid bungling use of a large TradeInfo constructor + * argument list. If consecutive argument values of the same type are not + * ordered correctly, the compiler won't complain but the resulting bugs could + * be hard to find and fix. + */ + public static class TradeInfoBuilder { + private OfferInfo offer; + private String tradeId; + private String shortId; + private String state; + private String phase; + private String tradePeriodState; + private boolean isDepositPublished; + private boolean isDepositConfirmed; + private boolean isFiatSent; + private boolean isFiatReceived; + private boolean isPayoutPublished; + private boolean isWithdrawn; + + public TradeInfoBuilder withOffer(OfferInfo offer) { + this.offer = offer; + return this; + } + + public TradeInfoBuilder withTradeId(String tradeId) { + this.tradeId = tradeId; + return this; + } + + public TradeInfoBuilder withShortId(String shortId) { + this.shortId = shortId; + return this; + } + + public TradeInfoBuilder withState(String state) { + this.state = state; + return this; + } + + public TradeInfoBuilder withPhase(String phase) { + this.phase = phase; + return this; + } + + public TradeInfoBuilder withTradePeriodState(String tradePeriodState) { + this.tradePeriodState = tradePeriodState; + return this; + } + + public TradeInfoBuilder withIsDepositPublished(boolean isDepositPublished) { + this.isDepositPublished = isDepositPublished; + return this; + } + + public TradeInfoBuilder withIsDepositConfirmed(boolean isDepositConfirmed) { + this.isDepositConfirmed = isDepositConfirmed; + return this; + } + + public TradeInfoBuilder withIsFiatSent(boolean isFiatSent) { + this.isFiatSent = isFiatSent; + return this; + } + + public TradeInfoBuilder withIsFiatReceived(boolean isFiatReceived) { + this.isFiatReceived = isFiatReceived; + return this; + } + + public TradeInfoBuilder withIsPayoutPublished(boolean isPayoutPublished) { + this.isPayoutPublished = isPayoutPublished; + return this; + } + + public TradeInfoBuilder withIsWithdrawn(boolean isWithdrawn) { + this.isWithdrawn = isWithdrawn; + return this; + } + + public TradeInfo build() { + return new TradeInfo(this); + } + } + + @Override + public String toString() { + return "TradeInfo{" + + " tradeId='" + tradeId + '\'' + "\n" + + ", shortId='" + shortId + '\'' + "\n" + + ", state='" + state + '\'' + "\n" + + ", phase='" + phase + '\'' + "\n" + + ", tradePeriodState='" + tradePeriodState + '\'' + "\n" + + ", isDepositPublished=" + isDepositPublished + "\n" + + ", isDepositConfirmed=" + isDepositConfirmed + "\n" + + ", isFiatSent=" + isFiatSent + "\n" + + ", isFiatReceived=" + isFiatReceived + "\n" + + ", isPayoutPublished=" + isPayoutPublished + "\n" + + ", isWithdrawn=" + isWithdrawn + "\n" + + ", offer=" + offer + "\n" + + '}'; + } +} diff --git a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java new file mode 100644 index 00000000000..97f58f44fe6 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -0,0 +1,293 @@ +package bisq.core.offer.takeoffer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferUtil; +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.common.taskrunner.Model; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import java.util.Objects; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static bisq.core.btc.model.AddressEntry.Context.OFFER_FUNDING; +import static bisq.core.offer.OfferPayload.Direction.SELL; +import static bisq.core.util.VolumeUtil.getAdjustedVolumeForHalCash; +import static bisq.core.util.VolumeUtil.getRoundedFiatVolume; +import static bisq.core.util.coin.CoinUtil.minCoin; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.core.Coin.ZERO; +import static org.bitcoinj.core.Coin.valueOf; + +@Slf4j +public class TakeOfferModel implements Model { + // Immutable + private final AccountAgeWitnessService accountAgeWitnessService; + private final BtcWalletService btcWalletService; + private final FeeService feeService; + private final OfferUtil offerUtil; + private final PriceFeedService priceFeedService; + + // Mutable + @Getter + private AddressEntry addressEntry; + @Getter + private Coin amount; + @Getter + private boolean isCurrencyForTakerFeeBtc; + private Offer offer; + private PaymentAccount paymentAccount; + @Getter + private Coin securityDeposit; + private boolean useSavingsWallet; + + // 260 kb is typical trade fee tx size with 1 input, but trade tx (deposit + payout) + // are larger so we adjust to 320. + private final int feeTxSize = 320; + private Coin txFeePerByteFromFeeService; + @Getter + private Coin txFeeFromFeeService; + @Getter + private Coin takerFee; + @Getter + private Coin totalToPayAsCoin; + @Getter + private Coin missingCoin = ZERO; + @Getter + private Coin totalAvailableBalance; + @Getter + private Coin balance; + @Getter + private boolean isBtcWalletFunded; + @Getter + private Volume volume; + + @Inject + public TakeOfferModel(AccountAgeWitnessService accountAgeWitnessService, + BtcWalletService btcWalletService, + FeeService feeService, + OfferUtil offerUtil, + PriceFeedService priceFeedService) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.btcWalletService = btcWalletService; + this.feeService = feeService; + this.offerUtil = offerUtil; + this.priceFeedService = priceFeedService; + } + + public void initModel(Offer offer, + PaymentAccount paymentAccount, + boolean useSavingsWallet) { + this.clearModel(); + this.offer = offer; + this.paymentAccount = paymentAccount; + this.addressEntry = btcWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); + validateModelInputs(); + + this.useSavingsWallet = useSavingsWallet; + this.amount = valueOf(Math.min(offer.getAmount().value, getMaxTradeLimit())); + this.securityDeposit = offer.getDirection() == SELL + ? offer.getBuyerSecurityDeposit() + : offer.getSellerSecurityDeposit(); + this.isCurrencyForTakerFeeBtc = offerUtil.isCurrencyForTakerFeeBtc(amount); + this.takerFee = offerUtil.getTakerFee(isCurrencyForTakerFeeBtc, amount); + + calculateTxFees(); + calculateVolume(); + calculateTotalToPay(); + offer.resetState(); + + priceFeedService.setCurrencyCode(offer.getCurrencyCode()); + } + + @Override + public void onComplete() { + // empty + } + + private void calculateTxFees() { + // Taker pays 3 times the tx fee (taker fee, deposit, payout) because the mining fee might be different when maker created the offer + // and reserved his funds. Taker creates at least taker fee and deposit tx at nearly the same moment. Just the payout will + // be later and still could lead to issues if the required fee changed a lot in the meantime. using RBF and/or + // multiple batch-signed payout tx with different fees might be an option but RBF is not supported yet in BitcoinJ + // and batched txs would add more complexity to the trade protocol. + + // A typical trade fee tx has about 260 bytes (if one input). The trade txs has about 336-414 bytes. + // We use 320 as a average value. + + // trade fee tx: 260 bytes (1 input) + // deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount) + // payout tx: 371 bytes + // disputed payout tx: 408 bytes + + // Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values) + // But the "take offer" happens usually after that so we should have already the value from the estimation service. + txFeePerByteFromFeeService = feeService.getTxFeePerByte(); + txFeeFromFeeService = offerUtil.getTxFeeBySize(txFeePerByteFromFeeService, feeTxSize); + + // We request to get the actual estimated fee + log.info("Start requestTxFee: txFeeFromFeeService={}", txFeeFromFeeService); + feeService.requestFees(() -> { + txFeePerByteFromFeeService = feeService.getTxFeePerByte(); + txFeeFromFeeService = offerUtil.getTxFeeBySize(txFeePerByteFromFeeService, feeTxSize); + calculateTotalToPay(); + }); + } + + private void calculateTotalToPay() { + // Taker pays 2 times the tx fee because the mining fee might be different when + // maker created the offer and reserved his funds, so that would not work well + // with dynamic fees. The mining fee for the takeOfferFee tx is deducted from + // the createOfferFee and not visible to the trader. + Coin feeAndSecDeposit = getTotalTxFee().add(securityDeposit); + if (isCurrencyForTakerFeeBtc) + feeAndSecDeposit = feeAndSecDeposit.add(takerFee); + + totalToPayAsCoin = offer.isBuyOffer() + ? feeAndSecDeposit.add(amount) + : feeAndSecDeposit; + + updateBalance(); + } + + private void calculateVolume() { + Price tradePrice = offer.getPrice(); + Volume volumeByAmount = Objects.requireNonNull(tradePrice).getVolumeByAmount(amount); + + if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = getAdjustedVolumeForHalCash(volumeByAmount); + else if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) + volumeByAmount = getRoundedFiatVolume(volumeByAmount); + + volume = volumeByAmount; + + updateBalance(); + } + + private void updateBalance() { + Coin tradeWalletBalance = btcWalletService.getBalanceForAddress(addressEntry.getAddress()); + if (useSavingsWallet) { + Coin savingWalletBalance = btcWalletService.getSavingWalletBalance(); + totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance); + if (totalToPayAsCoin != null) + balance = minCoin(totalToPayAsCoin, totalAvailableBalance); + + } else { + balance = tradeWalletBalance; + } + missingCoin = offerUtil.getBalanceShortage(totalToPayAsCoin, balance); + isBtcWalletFunded = offerUtil.isBalanceSufficient(totalToPayAsCoin, balance); + } + + private long getMaxTradeLimit() { + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), + offer.getMirroredDirection()); + } + + public Coin getTotalTxFee() { + Coin totalTxFees = txFeeFromFeeService.add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx()); + if (isCurrencyForTakerFeeBtc) + return totalTxFees; + else + return totalTxFees.subtract(takerFee); + } + + @NotNull + public Coin getFundsNeededForTrade() { + // If taking a buy offer, taker needs to reserve the offer.amt too. + return securityDeposit + .add(getTxFeeForDepositTx()) + .add(getTxFeeForPayoutTx()) + .add(offer.isBuyOffer() ? amount : ZERO); + } + + private Coin getTxFeeForDepositTx() { + // TODO fix with new trade protocol! + // Unfortunately we cannot change that to the correct fees as it would break + // backward compatibility. We still might find a way with offer version or app + // version checks so lets keep that commented out code as that shows how it + // should be. + return txFeeFromFeeService; + } + + private Coin getTxFeeForPayoutTx() { + // TODO fix with new trade protocol! + // Unfortunately we cannot change that to the correct fees as it would break + // backward compatibility. We still might find a way with offer version or app + // version checks so lets keep that commented out code as that shows how it + // should be. + return txFeeFromFeeService; + } + + private void validateModelInputs() { + checkNotNull(offer, "offer must not be null"); + checkNotNull(offer.getAmount(), "offer amount must not be null"); + checkArgument(offer.getAmount().value > 0, "offer amount must not be zero"); + checkNotNull(offer.getPrice(), "offer price must not be null"); + checkNotNull(paymentAccount, "payment account must not be null"); + checkNotNull(addressEntry, "address entry must not be null"); + } + + private void clearModel() { + this.addressEntry = null; + this.amount = null; + this.balance = null; + this.isBtcWalletFunded = false; + this.isCurrencyForTakerFeeBtc = false; + this.missingCoin = ZERO; + this.offer = null; + this.paymentAccount = null; + this.securityDeposit = null; + this.takerFee = null; + this.totalAvailableBalance = null; + this.totalToPayAsCoin = null; + this.txFeeFromFeeService = null; + this.txFeePerByteFromFeeService = null; + this.useSavingsWallet = true; + this.volume = null; + } + + @Override + public String toString() { + return "TakeOfferModel{" + + " offer.id=" + offer.getId() + "\n" + + " offer.state=" + offer.getState() + "\n" + + ", paymentAccount.id=" + paymentAccount.getId() + "\n" + + ", paymentAccount.method.id=" + paymentAccount.getPaymentMethod().getId() + "\n" + + ", useSavingsWallet=" + useSavingsWallet + "\n" + + ", addressEntry=" + addressEntry + "\n" + + ", amount=" + amount + "\n" + + ", securityDeposit=" + securityDeposit + "\n" + + ", feeTxSize=" + feeTxSize + "\n" + + ", txFeePerByteFromFeeService=" + txFeePerByteFromFeeService + "\n" + + ", txFeeFromFeeService=" + txFeeFromFeeService + "\n" + + ", takerFee=" + takerFee + "\n" + + ", totalToPayAsCoin=" + totalToPayAsCoin + "\n" + + ", missingCoin=" + missingCoin + "\n" + + ", totalAvailableBalance=" + totalAvailableBalance + "\n" + + ", balance=" + balance + "\n" + + ", volume=" + volume + "\n" + + ", fundsNeededForTrade=" + getFundsNeededForTrade() + "\n" + + ", isCurrencyForTakerFeeBtc=" + isCurrencyForTakerFeeBtc + "\n" + + ", isBtcWalletFunded=" + isBtcWalletFunded + "\n" + + '}'; + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index 2cee16b601d..d7785935563 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -36,11 +36,12 @@ import javax.inject.Inject; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import static bisq.core.api.model.OfferInfo.toOfferInfo; + @Slf4j class GrpcOffersService extends OffersGrpc.OffersImplBase { @@ -72,7 +73,7 @@ public void getOffer(GetOfferRequest req, public void getOffers(GetOffersRequest req, StreamObserver responseObserver) { List result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode()) - .stream().map(this::toOfferInfo) + .stream().map(OfferInfo::toOfferInfo) .collect(Collectors.toList()); var reply = GetOffersReply.newBuilder() .addAllOffers(result.stream() @@ -113,28 +114,4 @@ public void createOffer(CreateOfferRequest req, throw ex; } } - - // The client cannot see bisq.core.Offer or its fromProto method. - // We use the lighter weight OfferInfo proto wrapper instead, containing just - // enough fields to view and create offers. - private OfferInfo toOfferInfo(Offer offer) { - return new OfferInfo.OfferInfoBuilder() - .withId(offer.getId()) - .withDirection(offer.getDirection().name()) - .withPrice(Objects.requireNonNull(offer.getPrice()).getValue()) - .withUseMarketBasedPrice(offer.isUseMarketBasedPrice()) - .withMarketPriceMargin(offer.getMarketPriceMargin()) - .withAmount(offer.getAmount().value) - .withMinAmount(offer.getMinAmount().value) - .withVolume(Objects.requireNonNull(offer.getVolume()).getValue()) - .withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue()) - .withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value) - .withPaymentAccountId(offer.getMakerPaymentAccountId()) - .withPaymentMethodId(offer.getPaymentMethod().getId()) - .withPaymentMethodShortName(offer.getPaymentMethod().getShortName()) - .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) - .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) - .withDate(offer.getDate().getTime()) - .build(); - } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java index 4937e09092e..bb9dbebd273 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -45,6 +45,7 @@ public GrpcServer(Config config, GrpcPriceService priceService, GrpcVersionService versionService, GrpcGetTradeStatisticsService tradeStatisticsService, + GrpcTradesService tradesService, GrpcWalletsService walletsService) { this.server = ServerBuilder.forPort(config.apiPort) .addService(disputeAgentsService) @@ -52,6 +53,7 @@ public GrpcServer(Config config, .addService(paymentAccountsService) .addService(priceService) .addService(tradeStatisticsService) + .addService(tradesService) .addService(versionService) .addService(walletsService) .intercept(passwordAuthInterceptor) diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java new file mode 100644 index 00000000000..ccbe6438b94 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -0,0 +1,87 @@ +/* + * 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.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.api.model.TradeInfo; +import bisq.core.trade.Trade; + +import bisq.proto.grpc.GetTradeReply; +import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.TakeOfferReply; +import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.TradesGrpc; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.api.model.TradeInfo.toTradeInfo; + +@Slf4j +class GrpcTradesService extends TradesGrpc.TradesImplBase { + + private final CoreApi coreApi; + + @Inject + public GrpcTradesService(CoreApi coreApi) { + this.coreApi = coreApi; + } + + @Override + public void getTrade(GetTradeRequest req, + StreamObserver responseObserver) { + try { + Trade trade = coreApi.getTrade(req.getTradeId()); + var reply = GetTradeReply.newBuilder() + .setTrade(toTradeInfo(trade).toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } + + @Override + public void takeOffer(TakeOfferRequest req, + StreamObserver responseObserver) { + try { + coreApi.takeOffer(req.getOfferId(), + req.getPaymentAccountId(), + trade -> { + TradeInfo tradeInfo = toTradeInfo(trade); + var reply = TakeOfferReply.newBuilder() + .setTrade(tradeInfo.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } +} diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 1ed87f20853..38ddd4a9dbb 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -96,12 +96,13 @@ message OfferInfo { uint64 volume = 8; uint64 minVolume = 9; uint64 buyerSecurityDeposit = 10; - string paymentAccountId = 11; // only used when creating offer + string paymentAccountId = 11; string paymentMethodId = 12; string paymentMethodShortName = 13; string baseCurrencyCode = 14; string counterCurrencyCode = 15; uint64 date = 16; + string state = 17; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -166,6 +167,67 @@ message GetTradeStatisticsReply { repeated TradeStatistics3 TradeStatistics = 1; } +/////////////////////////////////////////////////////////////////////////////////////////// +// Trades +/////////////////////////////////////////////////////////////////////////////////////////// + +service Trades { + rpc GetTrade (GetTradeRequest) returns (GetTradeReply) { + } + rpc TakeOffer (TakeOfferRequest) returns (TakeOfferReply) { + } + rpc ConfirmPaymentStarted (ConfirmPaymentStartedRequest) returns (ConfirmPaymentStartedReply) { + } + rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) { + } +} + +message TakeOfferRequest { + string offerId = 1; + string paymentAccountId = 2; +} + +message TakeOfferReply { + TradeInfo trade = 1; +} + +message ConfirmPaymentStartedRequest { + string tradeId = 1; +} + +message ConfirmPaymentStartedReply { +} + +message ConfirmPaymentReceivedRequest { + string tradeId = 1; +} + +message ConfirmPaymentReceivedReply { +} + +message GetTradeRequest { + string tradeId = 1; +} + +message GetTradeReply { + TradeInfo trade = 1; +} + +message TradeInfo { + OfferInfo offer = 1; + string tradeId = 2; + string shortId = 3; + string state = 4; + string phase = 5; + string tradePeriodState = 6; + bool isDepositPublished = 7; + bool isDepositConfirmed = 8; + bool isFiatSent = 9; + bool isFiatReceived = 10; + bool isPayoutPublished = 11; + bool isWithdrawn = 12; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Wallets /////////////////////////////////////////////////////////////////////////////////////////// From 3d2b90fb9602f527d048432d9378c886683cdece Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 20 Oct 2020 16:20:40 -0300 Subject: [PATCH 3/9] Add 'confirmpaymentsent' api method - Implement confirmpaymentsent on server and cli side - Enable confirmpaymentsent method tests --- .../method/trade/TakeBuyBTCOfferTest.java | 1 - .../method/trade/TakeSellBTCOfferTest.java | 1 - cli/src/main/java/bisq/cli/CliMain.java | 15 ++++++++++++++ core/src/main/java/bisq/core/api/CoreApi.java | 4 ++++ .../java/bisq/core/api/CoreTradesService.java | 20 +++++++++++++++++++ .../bisq/daemon/grpc/GrpcTradesService.java | 17 ++++++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index ac46307ce1c..a83d4720bc7 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -95,7 +95,6 @@ public void testTakeAlicesBuyOffer() { } } - @Disabled @Test @Order(2) public void testAlicesConfirmPaymentStarted() { diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index c3b9a918be0..9d7dda8705f 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -97,7 +97,6 @@ public void testTakeAlicesSellOffer() { } } - @Disabled @Test @Order(2) public void testBobsConfirmPaymentStarted() { diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index c459c536cd4..9fe0fd605da 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetAddressBalanceRequest; @@ -72,6 +73,7 @@ private enum Method { getoffer, getoffers, takeoffer, + confirmpaymentstarted, createpaymentacct, getpaymentaccts, getversion, @@ -271,6 +273,18 @@ public static void run(String[] args) { out.printf("trade '%s' successfully taken", reply.getTrade().getShortId()); return; } + case confirmpaymentstarted: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("incorrect parameter count, expecting trade id"); + + var tradeId = nonOptionArgs.get(1); + var request = ConfirmPaymentStartedRequest.newBuilder() + .setTradeId(tradeId) + .build(); + tradesService.confirmPaymentStarted(request); + out.printf("trade '%s' payment started message sent", tradeId); + return; + } case createpaymentacct: { if (nonOptionArgs.size() < 5) throw new IllegalArgumentException( @@ -399,6 +413,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id"); stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); + stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started"); stream.format(rowFormat, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts"); stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet"); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 88e08bf6df5..1715a0f3fd6 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -181,6 +181,10 @@ public void takeOffer(String offerId, resultHandler); } + public void confirmPaymentStarted(String tradeId) { + coreTradesService.confirmPaymentStarted(tradeId); + } + public Trade getTrade(String tradeId) { return coreTradesService.getTrade(tradeId); } diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 731bccebdb4..deed77e6d16 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -21,6 +21,7 @@ import bisq.core.offer.takeoffer.TakeOfferModel; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; +import bisq.core.trade.protocol.BuyerProtocol; import bisq.core.user.User; import javax.inject.Inject; @@ -79,6 +80,21 @@ void takeOffer(Offer offer, ); } + void confirmPaymentStarted(String tradeId) { + var trade = getTradeWithId(tradeId); + if (isFollowingBuyerProtocol(trade)) { + var tradeProtocol = tradeManager.getTradeProtocol(trade); + ((BuyerProtocol) tradeProtocol).onPaymentStarted( + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + } + ); + } else { + throw new IllegalStateException("you are the seller and not sending payment"); + } + } Trade getTrade(String tradeId) { return getTradeWithId(tradeId); @@ -88,4 +104,8 @@ private Trade getTradeWithId(String tradeId) { return tradeManager.getTradeById(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); } + + private boolean isFollowingBuyerProtocol(Trade trade) { + return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol; + } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index ccbe6438b94..a72a486cb0a 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -21,6 +21,8 @@ import bisq.core.api.model.TradeInfo; import bisq.core.trade.Trade; +import bisq.proto.grpc.ConfirmPaymentStartedReply; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.GetTradeReply; import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.TakeOfferReply; @@ -84,4 +86,19 @@ public void takeOffer(TakeOfferRequest req, throw ex; } } + + @Override + public void confirmPaymentStarted(ConfirmPaymentStartedRequest req, + StreamObserver responseObserver) { + try { + coreApi.confirmPaymentStarted(req.getTradeId()); + var reply = ConfirmPaymentStartedReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } } From ac8ed8dd063ade72261942473f77a843a217c373 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 20 Oct 2020 16:51:48 -0300 Subject: [PATCH 4/9] Add 'confirmpaymentreceived' api method - Implement confirmpaymentsent on server and cli side - Enable confirmpaymentreceived method tests --- .../method/trade/TakeBuyBTCOfferTest.java | 2 -- .../method/trade/TakeSellBTCOfferTest.java | 2 -- cli/src/main/java/bisq/cli/CliMain.java | 15 +++++++++++++++ core/src/main/java/bisq/core/api/CoreApi.java | 4 ++++ .../java/bisq/core/api/CoreTradesService.java | 17 +++++++++++++++++ .../bisq/daemon/grpc/GrpcTradesService.java | 17 +++++++++++++++++ 6 files changed, 53 insertions(+), 4 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index a83d4720bc7..040cee5a18d 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -24,7 +24,6 @@ import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -113,7 +112,6 @@ public void testAlicesConfirmPaymentStarted() { } } - @Disabled @Test @Order(3) public void testBobsConfirmPaymentReceived() { diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index 9d7dda8705f..5347159daae 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -24,7 +24,6 @@ import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -116,7 +115,6 @@ public void testBobsConfirmPaymentStarted() { } } - @Disabled @Test @Order(3) public void testAlicesConfirmPaymentReceived() { diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 9fe0fd605da..d8f66dabbef 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -17,6 +17,7 @@ package bisq.cli; +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; @@ -74,6 +75,7 @@ private enum Method { getoffers, takeoffer, confirmpaymentstarted, + confirmpaymentreceived, createpaymentacct, getpaymentaccts, getversion, @@ -285,6 +287,18 @@ public static void run(String[] args) { out.printf("trade '%s' payment started message sent", tradeId); return; } + case confirmpaymentreceived: { + if (nonOptionArgs.size() < 2) + throw new IllegalArgumentException("incorrect parameter count, expecting trade id"); + + var tradeId = nonOptionArgs.get(1); + var request = ConfirmPaymentReceivedRequest.newBuilder() + .setTradeId(tradeId) + .build(); + tradesService.confirmPaymentReceived(request); + out.printf("trade '%s' payment received message sent", tradeId); + return; + } case createpaymentacct: { if (nonOptionArgs.size() < 5) throw new IllegalArgumentException( @@ -414,6 +428,7 @@ private static void printHelp(OptionParser parser, PrintStream stream) { stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers"); stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id"); stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started"); + stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received"); stream.format(rowFormat, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account"); stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts"); stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet"); diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 1715a0f3fd6..01f70d312f4 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -185,6 +185,10 @@ public void confirmPaymentStarted(String tradeId) { coreTradesService.confirmPaymentStarted(tradeId); } + public void confirmPaymentReceived(String tradeId) { + coreTradesService.confirmPaymentReceived(tradeId); + } + public Trade getTrade(String tradeId) { return coreTradesService.getTrade(tradeId); } diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index deed77e6d16..d5a664757c6 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -22,6 +22,7 @@ import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.protocol.BuyerProtocol; +import bisq.core.trade.protocol.SellerProtocol; import bisq.core.user.User; import javax.inject.Inject; @@ -96,6 +97,22 @@ void confirmPaymentStarted(String tradeId) { } } + void confirmPaymentReceived(String tradeId) { + var trade = getTradeWithId(tradeId); + if (isFollowingBuyerProtocol(trade)) { + throw new IllegalStateException("you are the buyer, and not receiving payment"); + } else { + var tradeProtocol = tradeManager.getTradeProtocol(trade); + ((SellerProtocol) tradeProtocol).onPaymentReceived( + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + } + ); + } + } + Trade getTrade(String tradeId) { return getTradeWithId(tradeId); } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index a72a486cb0a..0ffbd71f044 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -21,6 +21,8 @@ import bisq.core.api.model.TradeInfo; import bisq.core.trade.Trade; +import bisq.proto.grpc.ConfirmPaymentReceivedReply; +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; import bisq.proto.grpc.ConfirmPaymentStartedReply; import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.GetTradeReply; @@ -101,4 +103,19 @@ public void confirmPaymentStarted(ConfirmPaymentStartedRequest req, throw ex; } } + + @Override + public void confirmPaymentReceived(ConfirmPaymentReceivedRequest req, + StreamObserver responseObserver) { + try { + coreApi.confirmPaymentReceived(req.getTradeId()); + var reply = ConfirmPaymentReceivedReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (IllegalStateException | IllegalArgumentException cause) { + var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage())); + responseObserver.onError(ex); + throw ex; + } + } } From 2ae6bfaf51dfd49eb1e2b43ae9fe82eb7e869a99 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 21 Oct 2020 12:54:28 -0300 Subject: [PATCH 5/9] Change predicate names isPositiveXYZ -> isNonZeroXYZ Resolves issue found during https://github.com/bisq-network/bisq/pull/4672 review, and mentioned in comment https://github.com/bisq-network/bisq/pull/4672#discussion_r509318045 --- .../desktop/main/offer/MutableOfferDataModel.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 91506f00a11..966e54508b8 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -133,9 +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(); + private final Predicate> isNonZeroAmount = (c) -> c.get() != null && !c.get().isZero(); + private final Predicate> isNonZeroPrice = (p) -> p.get() != null && !p.get().isZero(); + private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -517,7 +517,7 @@ long getMaxTradeLimit() { } void calculateVolume() { - if (isPositivePrice.test(price) && isPositiveAmount.test(amount)) { + if (isNonZeroPrice.test(price) && isNonZeroAmount.test(amount)) { try { Volume volumeByAmount = calculateVolumeForAmount(amount); @@ -533,7 +533,7 @@ void calculateVolume() { } void calculateMinVolume() { - if (isPositivePrice.test(price) && isPositiveAmount.test(minAmount)) { + if (isNonZeroPrice.test(price) && isNonZeroAmount.test(minAmount)) { try { Volume volumeByAmount = calculateVolumeForAmount(minAmount); @@ -557,7 +557,7 @@ else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) } void calculateAmount() { - if (isPositivePrice.test(price) && isPositiveVolume.test(volume) && allowAmountUpdate) { + if (isNonZeroPrice.test(price) && isNonZeroVolume.test(volume) && allowAmountUpdate) { try { Coin value = DisplayUtils.reduceTo4Decimals(price.get().getAmountByVolume(volume.get()), btcFormatter); if (paymentAccount.isHalCashAccount()) From fa0e05a33620e2d04d429d30dfb9450af6ae84fa Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 22 Oct 2020 10:09:35 -0300 Subject: [PATCH 6/9] Remove redundant getTrade(id) method Resolves issue found during https://github.com/bisq-network/bisq/pull/4673 review, and mentioned in comment https://github.com/bisq-network/bisq/pull/4673#discussion_r510090257 --- core/src/main/java/bisq/core/api/CoreTradesService.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index d5a664757c6..560353e0e44 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -82,7 +82,7 @@ void takeOffer(Offer offer, } void confirmPaymentStarted(String tradeId) { - var trade = getTradeWithId(tradeId); + var trade = getTrade(tradeId); if (isFollowingBuyerProtocol(trade)) { var tradeProtocol = tradeManager.getTradeProtocol(trade); ((BuyerProtocol) tradeProtocol).onPaymentStarted( @@ -98,7 +98,7 @@ void confirmPaymentStarted(String tradeId) { } void confirmPaymentReceived(String tradeId) { - var trade = getTradeWithId(tradeId); + var trade = getTrade(tradeId); if (isFollowingBuyerProtocol(trade)) { throw new IllegalStateException("you are the buyer, and not receiving payment"); } else { @@ -114,10 +114,6 @@ void confirmPaymentReceived(String tradeId) { } Trade getTrade(String tradeId) { - return getTradeWithId(tradeId); - } - - private Trade getTradeWithId(String tradeId) { return tradeManager.getTradeById(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); } From 31a311903ab985c2b66f99f8dbf7c59ee0733d65 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 22 Oct 2020 10:17:12 -0300 Subject: [PATCH 7/9] Simplify result handler argument Resolves issue found during https://github.com/bisq-network/bisq/pull/4673 review, and suggested in comment https://github.com/bisq-network/bisq/pull/4673#discussion_r510089605 --- core/src/main/java/bisq/core/api/CoreTradesService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 560353e0e44..d0200a78ce5 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -71,9 +71,7 @@ void takeOffer(Offer offer, offer, paymentAccountId, useSavingsWallet, - trade -> { - resultHandler.accept(trade); - }, + resultHandler::accept, errorMessage -> { log.error(errorMessage); throw new IllegalStateException(errorMessage); From d463dd14a00a3b3b40ddf1a8c189b896d80bc73e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 22 Oct 2020 10:41:04 -0300 Subject: [PATCH 8/9] Remove useless default tx fee calculations Resolves issue found during https://github.com/bisq-network/bisq/pull/4673 review, and suggested in comment https://github.com/bisq-network/bisq/pull/4673#discussion_r510110682 Also shortened comment lines to < 90 chars. --- .../core/offer/takeoffer/TakeOfferModel.java | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java index 97f58f44fe6..8580bcd1856 100644 --- a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -123,26 +123,25 @@ public void onComplete() { } private void calculateTxFees() { - // Taker pays 3 times the tx fee (taker fee, deposit, payout) because the mining fee might be different when maker created the offer - // and reserved his funds. Taker creates at least taker fee and deposit tx at nearly the same moment. Just the payout will - // be later and still could lead to issues if the required fee changed a lot in the meantime. using RBF and/or - // multiple batch-signed payout tx with different fees might be an option but RBF is not supported yet in BitcoinJ - // and batched txs would add more complexity to the trade protocol. - - // A typical trade fee tx has about 260 bytes (if one input). The trade txs has about 336-414 bytes. - // We use 320 as a average value. - - // trade fee tx: 260 bytes (1 input) - // deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount) - // payout tx: 371 bytes - // disputed payout tx: 408 bytes - - // Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values) - // But the "take offer" happens usually after that so we should have already the value from the estimation service. - txFeePerByteFromFeeService = feeService.getTxFeePerByte(); - txFeeFromFeeService = offerUtil.getTxFeeBySize(txFeePerByteFromFeeService, feeTxSize); - - // We request to get the actual estimated fee + // Taker pays 3 times the tx fee (taker fee, deposit, payout) because the mining + // fee might be different when maker created the offer and reserved his funds. + // Taker creates at least taker fee and deposit tx at nearly the same moment. + // Just the payout will be later and still could lead to issues if the required + // fee changed a lot in the meantime. using RBF and/or multiple batch-signed + // payout tx with different fees might be an option but RBF is not supported yet + // in BitcoinJ and batched txs would add more complexity to the trade protocol. + + // A typical trade fee tx has about 260 bytes (if one input). The trade txs has + // about 336-414 bytes. We use 320 as a average value. + + // Fee calculations: + // Trade fee tx: 260 bytes (1 input) + // Deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes + // (1 MS output + OP_RETURN + change in case of smaller trade amount) + // Payout tx: 371 bytes + // Disputed payout tx: 408 bytes + + // Request actual fees: log.info("Start requestTxFee: txFeeFromFeeService={}", txFeeFromFeeService); feeService.requestFees(() -> { txFeePerByteFromFeeService = feeService.getTxFeePerByte(); From 1f3554ef65e5e891635017a7f8f6aacabe299cb9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 22 Oct 2020 10:55:53 -0300 Subject: [PATCH 9/9] Remove redundant calculateTotalToPay() call Resolves issue found during https://github.com/bisq-network/bisq/pull/4673 review, and suggested in comment https://github.com/bisq-network/bisq/pull/4673#discussion_r510111662 --- core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java index 8580bcd1856..efd0523ec19 100644 --- a/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -146,7 +146,6 @@ private void calculateTxFees() { feeService.requestFees(() -> { txFeePerByteFromFeeService = feeService.getTxFeePerByte(); txFeeFromFeeService = offerUtil.getTxFeeBySize(txFeePerByteFromFeeService, feeTxSize); - calculateTotalToPay(); }); }