From c354a6986a38287e47cb96dc3dcd51852d2155cc Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:31:32 -0300 Subject: [PATCH 01/13] Refactor OfferUtil into an injected service This is a preliminary refactoring for a new core TakeOfferModel, which is going to need logic (for api 'takeoffer') currently located in the unaccessible desktop module's offer package. This change injects service dependencies into the OfferUtil instance and removes those service dependency arguments from formerly static method signatures. Other changes include: - Moving some Volume calc methods from OfferUtil to VolumeUtil. - Moving some Coin calc methods from OfferUtil to CoinUtil. - Injecting OfferUtil into CreateOfferService and relevant offer data & view models. - Minor changes to TradeStatistics2, Trade, Offer & Contract, to use static methods moved from OfferUtil to the new VolumeUtil class. - Deletion of bisq.desktop.main.offer.MakerFeeProvider, and bisq.core.offer.MakerFeeProvider. They were unnecessary wrappers around a single OfferUtil method. - Renaming OfferUtilTest -> CoinUtilTest. - Moving tests in CoinCryptoUtilsTest to CoinUtilTest, and deleting CoinCryptoUtilsTest. - Adjusting affected offer data & view model tests. --- .../bisq/core/offer/CreateOfferService.java | 22 +- .../bisq/core/offer/MakerFeeProvider.java | 29 --- core/src/main/java/bisq/core/offer/Offer.java | 5 +- .../main/java/bisq/core/offer/OfferUtil.java | 201 ++++-------------- .../main/java/bisq/core/trade/Contract.java | 6 +- core/src/main/java/bisq/core/trade/Trade.java | 6 +- .../trade/statistics/TradeStatistics2.java | 4 +- .../main/java/bisq/core/util/VolumeUtil.java | 50 +++++ .../java/bisq/core/util/coin/CoinUtil.java | 108 ++++++++++ .../bisq/core/util/CoinCryptoUtilsTest.java | 60 ------ .../coin/CoinUtilTest.java} | 72 ++++--- .../desktop/main/offer/MakerFeeProvider.java | 13 -- .../main/offer/MutableOfferDataModel.java | 29 +-- .../main/offer/MutableOfferViewModel.java | 25 ++- .../createoffer/CreateOfferDataModel.java | 6 +- .../createoffer/CreateOfferViewModel.java | 6 +- .../offer/takeoffer/TakeOfferDataModel.java | 18 +- .../offer/takeoffer/TakeOfferViewModel.java | 22 +- .../editoffer/EditOfferDataModel.java | 6 +- .../editoffer/EditOfferViewModel.java | 6 +- .../createoffer/CreateOfferDataModelTest.java | 13 +- .../createoffer/CreateOfferViewModelTest.java | 9 +- .../editoffer/EditOfferDataModelTest.java | 8 +- 23 files changed, 351 insertions(+), 373 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} (51%) 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..e814d0fee65 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -63,7 +63,6 @@ @Singleton public class CreateOfferService { private final TxFeeEstimationService txFeeEstimationService; - private final MakerFeeProvider makerFeeProvider; private final BsqWalletService bsqWalletService; private final Preferences preferences; private final PriceFeedService priceFeedService; @@ -74,6 +73,7 @@ public class CreateOfferService { private final PubKeyRing pubKeyRing; private final User user; private final BtcWalletService btcWalletService; + private final OfferUtil offerUtil; /////////////////////////////////////////////////////////////////////////////////////////// @@ -82,7 +82,6 @@ public class CreateOfferService { @Inject public CreateOfferService(TxFeeEstimationService txFeeEstimationService, - MakerFeeProvider makerFeeProvider, BsqWalletService bsqWalletService, Preferences preferences, PriceFeedService priceFeedService, @@ -92,9 +91,9 @@ public CreateOfferService(TxFeeEstimationService txFeeEstimationService, P2PService p2PService, PubKeyRing pubKeyRing, User user, - BtcWalletService btcWalletService) { + BtcWalletService btcWalletService, + OfferUtil offerUtil) { this.txFeeEstimationService = txFeeEstimationService; - this.makerFeeProvider = makerFeeProvider; this.bsqWalletService = bsqWalletService; this.preferences = preferences; this.priceFeedService = priceFeedService; @@ -105,6 +104,7 @@ public CreateOfferService(TxFeeEstimationService txFeeEstimationService, this.pubKeyRing = pubKeyRing; this.user = user; this.btcWalletService = btcWalletService; + this.offerUtil = offerUtil; } @@ -186,7 +186,7 @@ public Offer createAndGetOffer(String offerId, 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); + boolean isCurrencyForMakerFeeBtc = offerUtil.isCurrencyForMakerFeeBtc(amount); Coin buyerSecurityDepositAsCoin = getBuyerSecurityDeposit(amount, buyerSecurityDepositAsDouble); Coin sellerSecurityDepositAsCoin = getSellerSecurityDeposit(amount, sellerSecurityDeposit); long maxTradeLimit = getMaxTradeLimit(paymentAccount, currencyCode, direction); @@ -200,16 +200,12 @@ public Offer createAndGetOffer(String offerId, long lowerClosePrice = 0; long upperClosePrice = 0; String hashOfChallenge = null; - Map extraDataMap = OfferUtil.getExtraDataMap(accountAgeWitnessService, - referralIdService, + Map extraDataMap = offerUtil.getExtraDataMap( paymentAccount, currencyCode, - preferences, direction); - OfferUtil.validateOfferData(filterManager, - p2PService, - buyerSecurityDepositAsDouble, + offerUtil.validateOfferData(buyerSecurityDepositAsDouble, paymentAccount, currencyCode, makerFeeAsCoin); @@ -295,7 +291,7 @@ public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) { } public Coin getMakerFee(Coin amount) { - return makerFeeProvider.getMakerFee(bsqWalletService, preferences, amount); + return offerUtil.getMakerFee(amount); } public long getMaxTradeLimit(PaymentAccount paymentAccount, @@ -309,7 +305,7 @@ public long getMaxTradeLimit(PaymentAccount paymentAccount, } public boolean isBuyOffer(OfferPayload.Direction direction) { - return OfferUtil.isBuyOffer(direction); + return offerUtil.isBuyOffer(direction); } 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..55363a70d4d 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -22,6 +22,7 @@ import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; import bisq.core.offer.availability.OfferAvailabilityModel; import bisq.core.offer.availability.OfferAvailabilityProtocol; import bisq.core.payment.payload.PaymentMethod; @@ -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..1cace6a316d 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -44,7 +44,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; @@ -65,76 +66,78 @@ * GUI and the API. */ @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 */ - public static boolean isBuyOffer(OfferPayload.Direction direction) { + public boolean isBuyOffer(OfferPayload.Direction direction) { return direction == OfferPayload.Direction.BUY; } /** * Returns the makerFee as Coin, this can be priced in BTC or BSQ. * - * @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} */ @Nullable - public static Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, @Nullable Coin amount) { - boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount); - return getMakerFee(isCurrencyForMakerFeeBtc, amount); - } - - /** - * 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 = CoinUtil.getFeePerBtc(FeeService.getMakerFeePerBtc(isCurrencyForMakerFeeBtc), amount); - return CoinUtil.maxCoin(feePerBtc, FeeService.getMinMakerFee(isCurrencyForMakerFeeBtc)); - } else { - return null; - } + public Coin getMakerFee(@Nullable Coin amount) { + boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(amount); + return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount); } /** * 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 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 */ - public static boolean isCurrencyForMakerFeeBtc(Preferences preferences, - BsqWalletService bsqWalletService, - @Nullable Coin amount) { + 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. @@ -150,7 +153,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,15 +162,13 @@ 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); @@ -183,125 +184,18 @@ public static boolean isBsqForTakerFeeAvailable(BsqWalletService bsqWalletServic 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, + 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, + 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. @@ -331,11 +225,8 @@ private static Optional getFeeInUserFiatCurrency(Coin makerFee, boolean } - public static Map getExtraDataMap(AccountAgeWitnessService accountAgeWitnessService, - ReferralIdService referralIdService, - PaymentAccount paymentAccount, + public Map getExtraDataMap(PaymentAccount paymentAccount, String currencyCode, - Preferences preferences, OfferPayload.Direction direction) { Map extraDataMap = new HashMap<>(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { @@ -364,9 +255,7 @@ public static Map getExtraDataMap(AccountAgeWitnessService accou return extraDataMap.isEmpty() ? null : extraDataMap; } - public static void validateOfferData(FilterManager filterManager, - P2PService p2PService, - double buyerSecurityDeposit, + public void validateOfferData(double buyerSecurityDeposit, PaymentAccount paymentAccount, String currencyCode, Coin makerFeeAsCoin) { diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java index 174719b0651..71c186de40f 100644 --- a/core/src/main/java/bisq/core/trade/Contract.java +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -20,8 +20,8 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; 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; @@ -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..efcb1b74e6a 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -21,8 +21,8 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; 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; @@ -831,9 +831,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 { 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 17a878c270c..78e07c190fd 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -21,8 +21,8 @@ import bisq.core.monetary.AltcoinExchangeRate; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferUtil; import bisq.network.p2p.storage.payload.CapabilityRequiringPayload; import bisq.network.p2p.storage.payload.PersistableNetworkPayload; @@ -275,7 +275,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..fcd7e6df004 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,100 @@ 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 + 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); + } } 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 51% 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..a6714a6f15a 100644 --- a/core/src/test/java/bisq/core/offer/OfferUtilTest.java +++ b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java @@ -1,21 +1,4 @@ -/* - * 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; +package bisq.core.util.coin; import bisq.core.monetary.Price; @@ -24,53 +7,82 @@ import org.junit.Assert; import org.junit.Test; -public class OfferUtilTest { +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +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 +93,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 44371697167..b869b1aa83c 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -33,6 +33,7 @@ import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -102,8 +103,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs final String shortOfferId; private final AccountAgeWitnessService accountAgeWitnessService; private final FeeService feeService; + protected final OfferUtil offerUtil; private final CoinFormatter btcFormatter; - private final MakerFeeProvider makerFeeProvider; private final Navigation navigation; private final String offerId; private final BalanceListener btcBalanceListener; @@ -149,8 +150,8 @@ public MutableOfferDataModel(CreateOfferService createOfferService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, FeeService feeService, + OfferUtil offerUtil, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - MakerFeeProvider makerFeeProvider, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(btcWalletService); @@ -164,8 +165,8 @@ public MutableOfferDataModel(CreateOfferService createOfferService, this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.feeService = feeService; + this.offerUtil = offerUtil; this.btcFormatter = btcFormatter; - this.makerFeeProvider = makerFeeProvider; this.navigation = navigation; this.tradeStatisticsManager = tradeStatisticsManager; @@ -560,9 +561,9 @@ private Volume calculateVolumeForAmount(ObjectProperty minAmount) { // For HalCash we want multiple of 10 EUR if (isHalCashAccount()) - volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount); + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount); + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); return volumeByAmount; } @@ -575,9 +576,9 @@ void calculateAmount() { try { Coin value = DisplayUtils.reduceTo4Decimals(price.get().getAmountByVolume(volume.get()), btcFormatter); if (isHalCashAccount()) - value = OfferUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit()); + 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(); @@ -609,7 +610,7 @@ Coin getSecurityDeposit() { } public boolean isBuyOffer() { - return OfferUtil.isBuyOffer(getDirection()); + return offerUtil.isBuyOffer(getDirection()); } public Coin getTxFee() { @@ -734,23 +735,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,7 +759,7 @@ boolean isPreferredFeeCurrencyBtc() { } boolean isBsqForFeeAvailable() { - return OfferUtil.isBsqForMakerFeeAvailable(bsqWalletService, amount.get()); + return offerUtil.isBsqForMakerFeeAvailable(amount.get()); } public boolean isHalCashAccount() { 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..3cb5aa7b562 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -43,6 +43,7 @@ import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferRestrictions; @@ -56,6 +57,7 @@ import bisq.core.util.ParsingUtils; 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; @@ -101,6 +103,7 @@ public abstract class MutableOfferViewModel ext private final Preferences preferences; protected final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; + protected final OfferUtil offerUtil; private final FiatVolumeValidator fiatVolumeValidator; private final FiatPriceValidator fiatPriceValidator; private final AltcoinValidator altcoinValidator; @@ -197,6 +200,7 @@ public MutableOfferViewModel(M dataModel, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, + OfferUtil offerUtil, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @@ -211,6 +215,7 @@ public MutableOfferViewModel(M dataModel, this.bsqValidator = bsqValidator; this.securityDepositValidator = securityDepositValidator; this.priceFeedService = priceFeedService; + this.offerUtil = offerUtil; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.preferences = preferences; @@ -498,8 +503,8 @@ 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,8 +513,8 @@ private void applyMakerFee() { } Coin makerFeeInBsq = dataModel.getMakerFeeInBsq(); - 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); @@ -850,9 +855,9 @@ void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { if (volume != null) { // For HalCash we want multiple of 10 EUR if (dataModel.isHalCashAccount()) - volume = OfferUtil.getAdjustedVolumeForHalCash(volume); + volume = VolumeUtil.getAdjustedVolumeForHalCash(volume); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) - volume = OfferUtil.getRoundedFiatVolume(volume); + volume = VolumeUtil.getRoundedFiatVolume(volume); this.volume.set(DisplayUtils.formatVolume(volume)); } @@ -1087,9 +1092,9 @@ private void setAmountToModel() { Price price = dataModel.getPrice().get(); if (price != null) { if (dataModel.isHalCashAccount()) - amount = OfferUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); + 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 || @@ -1111,9 +1116,9 @@ private void setMinAmountToModel() { long maxTradeLimit = dataModel.getMaxTradeLimit(); if (price != null) { if (dataModel.isHalCashAccount()) - minAmount = OfferUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit); + 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/createoffer/CreateOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java index c0f5e822f24..0004f87ea55 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; @@ -62,8 +62,8 @@ public CreateOfferDataModel(CreateOfferService createOfferService, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, FeeService feeService, + OfferUtil offerUtil, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - MakerFeeProvider makerFeeProvider, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(createOfferService, @@ -76,8 +76,8 @@ public CreateOfferDataModel(CreateOfferService createOfferService, priceFeedService, accountAgeWitnessService, feeService, + offerUtil, 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..b9b98174df0 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; @@ -49,6 +50,7 @@ public CreateOfferViewModel(CreateOfferDataModel dataModel, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, + OfferUtil offerUtil, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @@ -62,9 +64,11 @@ public CreateOfferViewModel(CreateOfferDataModel dataModel, bsqValidator, securityDepositValidator, priceFeedService, + offerUtil, accountAgeWitnessService, navigation, preferences, - btcFormatter, bsqFormatter); + btcFormatter, + bsqFormatter); } } 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 8e2c92fc536..5733d69b408 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 @@ -35,6 +35,7 @@ import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.util.VolumeUtil; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; @@ -83,6 +84,7 @@ class TakeOfferDataModel extends OfferDataModel { private final TradeManager tradeManager; private final OfferBook offerBook; + private final OfferUtil offerUtil; private final BsqWalletService bsqWalletService; private final User user; private final FeeService feeService; @@ -123,6 +125,7 @@ class TakeOfferDataModel extends OfferDataModel { @Inject TakeOfferDataModel(TradeManager tradeManager, OfferBook offerBook, + OfferUtil offerUtil, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, User user, FeeService feeService, @@ -138,6 +141,7 @@ class TakeOfferDataModel extends OfferDataModel { this.tradeManager = tradeManager; this.offerBook = offerBook; + this.offerUtil = offerUtil; this.bsqWalletService = bsqWalletService; this.user = user; this.feeService = feeService; @@ -461,9 +465,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); @@ -645,7 +649,7 @@ public boolean isHalCashAccount() { } public boolean isCurrencyForTakerFeeBtc() { - return OfferUtil.isCurrencyForTakerFeeBtc(preferences, bsqWalletService, amount.get()); + return offerUtil.isCurrencyForTakerFeeBtc(amount.get()); } public void setPreferredCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) { @@ -657,18 +661,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..a153cd84a0e 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 @@ -47,6 +47,7 @@ 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; @@ -94,6 +95,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private final Navigation navigation; private final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; + private final OfferUtil offerUtil; private String amountRange; private String paymentLabel; @@ -153,7 +155,8 @@ public TakeOfferViewModel(TakeOfferDataModel dataModel, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, - BsqFormatter bsqFormatter) { + BsqFormatter bsqFormatter, + OfferUtil offerUtil) { super(dataModel); this.dataModel = dataModel; @@ -165,6 +168,7 @@ public TakeOfferViewModel(TakeOfferDataModel dataModel, this.navigation = navigation; this.btcFormatter = btcFormatter; this.bsqFormatter = bsqFormatter; + this.offerUtil = offerUtil; createListeners(); } @@ -296,8 +300,8 @@ 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 +310,8 @@ 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 +359,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 +368,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 +642,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); 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..252e953d403 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; @@ -72,9 +72,9 @@ class EditOfferDataModel extends MutableOfferDataModel { PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, FeeService feeService, + OfferUtil offerUtil, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, CorePersistenceProtoResolver corePersistenceProtoResolver, - MakerFeeProvider makerFeeProvider, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { super(createOfferService, @@ -87,8 +87,8 @@ class EditOfferDataModel extends MutableOfferDataModel { priceFeedService, accountAgeWitnessService, feeService, + offerUtil, 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..6669b19ccc6 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; @@ -52,6 +53,7 @@ public EditOfferViewModel(EditOfferDataModel dataModel, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, + OfferUtil offerUtil, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, @@ -65,10 +67,12 @@ public EditOfferViewModel(EditOfferDataModel dataModel, bsqValidator, securityDepositValidator, priceFeedService, + offerUtil, accountAgeWitnessService, navigation, preferences, - btcFormatter, bsqFormatter); + btcFormatter, + bsqFormatter); 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..22d053b0a37 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; @@ -10,6 +8,7 @@ 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; @@ -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,10 @@ 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); + feeService, offerUtil,null, tradeStats, null); } @Test @@ -84,7 +83,7 @@ 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")); assertEquals("USD", model.getTradeCurrencyCode().get()); @@ -104,7 +103,7 @@ 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")); 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..935d7a6d4f0 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; @@ -33,6 +32,7 @@ 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; @@ -97,6 +97,7 @@ 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); @@ -117,13 +118,13 @@ public void setUp() { CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, null, btcWalletService, bsqWalletService, empty, user, null, priceFeedService, - accountAgeWitnessService, feeService, - coinFormatter, mock(MakerFeeProvider.class), tradeStats, null); + accountAgeWitnessService, feeService, offerUtil, + coinFormatter, tradeStats, null); dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin")); dataModel.activate(); model = new CreateOfferViewModel(dataModel, null, fiatPriceValidator, altcoinValidator, - btcValidator, null, securityDepositValidator, priceFeedService, null, null, + btcValidator, null, securityDepositValidator, priceFeedService, offerUtil,null, null, preferences, coinFormatter, bsqFormatter); 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..d8168f57619 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; @@ -40,6 +40,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import static bisq.desktop.maker.OfferMaker.Offer; import static bisq.desktop.maker.OfferMaker.btcBCHCOffer; import static bisq.desktop.maker.PreferenceMakers.empty; import static com.natpryce.makeiteasy.MakeItEasy.make; @@ -77,6 +78,7 @@ 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)); @@ -95,8 +97,8 @@ public void setUp() { model = new EditOfferDataModel(createOfferService, null, btcWalletService, bsqWalletService, empty, user, null, priceFeedService, - accountAgeWitnessService, feeService, null, null, - mock(MakerFeeProvider.class), mock(TradeStatisticsManager.class), null); + accountAgeWitnessService, feeService, offerUtil,null, null, + mock(TradeStatisticsManager.class), null); } @Test From 5fc50630e1f3bc57b318f6ecce212e4e113b5e96 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:53:58 -0300 Subject: [PATCH 02/13] Reformat, tidy up OfferUtil --- .../main/java/bisq/core/offer/OfferUtil.java | 95 +++++++++---------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 1cace6a316d..615631fbb58 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -39,7 +39,6 @@ import bisq.network.p2p.P2PService; import bisq.common.app.Capabilities; -import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; @@ -55,15 +54,14 @@ import javax.annotation.Nullable; +import static bisq.common.util.MathUtils.roundDoubleToLong; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; +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 and taking an Offer. */ @Slf4j @Singleton @@ -98,17 +96,19 @@ public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, * 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 boolean isBuyOffer(OfferPayload.Direction direction) { - return direction == OfferPayload.Direction.BUY; + public boolean isBuyOffer(Direction direction) { + return direction == Direction.BUY; } /** * Returns the makerFee as Coin, this can be priced in BTC or 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} + * @return the maker fee for the given trade amount, or {@code null} if the amount + * is {@code null} */ @Nullable public Coin getMakerFee(@Nullable Coin amount) { @@ -117,11 +117,12 @@ public Coin getMakerFee(@Nullable Coin amount) { } /** - * 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. + * 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 + * @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(); @@ -139,8 +140,9 @@ public boolean isBsqForMakerFeeAvailable(@Nullable Coin amount) { Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance(); 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; @@ -172,8 +174,9 @@ 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; @@ -184,8 +187,9 @@ public boolean isBsqForTakerFeeAvailable(@Nullable Coin amount) { return !availableBalance.subtract(takerFee).isNegative(); } - public Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc, - 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, @@ -194,15 +198,16 @@ public Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurren bsqFormatter); } - 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. - + 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 = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); + long marketPriceAsLong = roundDoubleToLong( + scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT)); Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong); if (isCurrencyForMakerFeeBtc) { @@ -224,41 +229,41 @@ private Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurre } } - public Map getExtraDataMap(PaymentAccount paymentAccount, - String currencyCode, - OfferPayload.Direction direction) { + 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 void validateOfferData(double buyerSecurityDeposit, - PaymentAccount paymentAccount, - String currencyCode, - Coin makerFeeAsCoin) { + 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(), @@ -272,14 +277,4 @@ public void validateOfferData(double buyerSecurityDeposit, 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); - - return needed; - }*/ } From 0d1141762b6f8c2f47e65551b440a32df515d0b3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:15:39 -0300 Subject: [PATCH 03/13] Add license comment, reformat CoinUtilTest --- .../bisq/core/util/coin/CoinUtilTest.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java index a6714a6f15a..d4c2e683ef0 100644 --- a/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java +++ b/core/src/test/java/bisq/core/util/coin/CoinUtilTest.java @@ -1,14 +1,30 @@ +/* + * 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.coin; import bisq.core.monetary.Price; import org.bitcoinj.core.Coin; -import org.junit.Assert; import org.junit.Test; -import static org.junit.Assert.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; public class CoinUtilTest { From baff9a960037cdc924e1e57ae855f51ee5591d89 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:32:05 -0300 Subject: [PATCH 04/13] Reformat desktop createoffer view & data model tests --- .../createoffer/CreateOfferDataModelTest.java | 25 +++++++---- .../createoffer/CreateOfferViewModelTest.java | 41 +++++++++++++++---- 2 files changed, 50 insertions(+), 16 deletions(-) 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 22d053b0a37..185d559ef2d 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 @@ -7,7 +7,6 @@ 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; @@ -28,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; @@ -63,10 +63,20 @@ public void setUp() { when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString()); when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); - model = new CreateOfferDataModel(createOfferService, null, btcWalletService, - null, preferences, user, null, - priceFeedService, null, - feeService, offerUtil,null, tradeStats, null); + model = new CreateOfferDataModel(createOfferService, + null, + btcWalletService, + null, + preferences, + user, + null, + priceFeedService, + null, + feeService, + offerUtil, + null, + tradeStats, + null); } @Test @@ -85,7 +95,7 @@ public void testUseTradeCurrencySetInOfferViewWhenInPaymentAccountAvailable() { when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); 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()); } @@ -105,8 +115,7 @@ public void testUseTradeAccountThatMatchesTradeCurrencySetInOffer() { when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount); 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 935d7a6d4f0..1020bc2a57f 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 @@ -103,7 +103,11 @@ public void setUp() { 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)); @@ -116,16 +120,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, offerUtil, - coinFormatter, tradeStats, null); + CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, + null, + btcWalletService, + bsqWalletService, + empty, + user, + null, + priceFeedService, + accountAgeWitnessService, + feeService, + offerUtil, + coinFormatter, + tradeStats, + null); dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin")); dataModel.activate(); - model = new CreateOfferViewModel(dataModel, null, fiatPriceValidator, altcoinValidator, - btcValidator, null, securityDepositValidator, priceFeedService, offerUtil,null, null, - preferences, coinFormatter, bsqFormatter); + model = new CreateOfferViewModel(dataModel, + null, + fiatPriceValidator, + altcoinValidator, + btcValidator, + null, + securityDepositValidator, + priceFeedService, + offerUtil, + null, + null, + preferences, + coinFormatter, + bsqFormatter); model.activate(); } From b672f4adebc9cb7551ecccaa2a108739423303e0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:34:39 -0300 Subject: [PATCH 05/13] Reformat desktop EditOfferDataModelTest --- .../editoffer/EditOfferDataModelTest.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) 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 d8168f57619..66c5d8e0b78 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 @@ -40,7 +40,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import static bisq.desktop.maker.OfferMaker.Offer; import static bisq.desktop.maker.OfferMaker.btcBCHCOffer; import static bisq.desktop.maker.PreferenceMakers.empty; import static com.natpryce.makeiteasy.MakeItEasy.make; @@ -83,7 +82,11 @@ public void setUp() { 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()); @@ -94,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, offerUtil,null, null, - mock(TradeStatisticsManager.class), null); + model = new EditOfferDataModel(createOfferService, + null, + btcWalletService, + bsqWalletService, + empty, + user, + null, + priceFeedService, + accountAgeWitnessService, + feeService, + offerUtil, + null, + null, + mock(TradeStatisticsManager.class), + null); } @Test From 149b22887f4c1960e57b9e62ee91669bffdc2da9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 18:46:44 -0300 Subject: [PATCH 06/13] Resolve argument reassignment issue (found by codacy) --- core/src/main/java/bisq/core/util/coin/CoinUtil.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 fcd7e6df004..17e0195aad1 100644 --- a/core/src/main/java/bisq/core/util/coin/CoinUtil.java +++ b/core/src/main/java/bisq/core/util/coin/CoinUtil.java @@ -160,20 +160,21 @@ static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int ); smallestUnitForAmount = Coin.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.value)); // We don't allow smaller amount values than smallestUnitForAmount - if (amount.compareTo(smallestUnitForAmount) < 0) - amount = smallestUnitForAmount; + boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0; // We get the adjusted volume from our amount - Volume volume = getAdjustedFiatVolume(price.getVolumeByAmount(amount), factor); + 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. - amount = price.getAmountByVolume(volume); + Coin amountByVolume = price.getAmountByVolume(volume); // For the amount we allow only 4 decimal places - long adjustedAmount = Math.round((double) amount.value / 10000d) * 10000; + 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) { From 2bf4b0af9d696b2257393d3f9ac7ec85fab49ada Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Oct 2020 18:48:06 -0300 Subject: [PATCH 07/13] Remove unused final fields (found by codacy) --- .../java/bisq/core/offer/CreateOfferService.java | 15 --------------- .../main/offer/takeoffer/TakeOfferViewModel.java | 11 +---------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index e814d0fee65..15f0610e77e 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -19,10 +19,8 @@ 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; @@ -31,7 +29,6 @@ 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; @@ -63,12 +60,8 @@ @Singleton public class CreateOfferService { private final TxFeeEstimationService txFeeEstimationService; - 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; @@ -82,24 +75,16 @@ public class CreateOfferService { @Inject public CreateOfferService(TxFeeEstimationService txFeeEstimationService, - BsqWalletService bsqWalletService, - Preferences preferences, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, - ReferralIdService referralIdService, - FilterManager filterManager, P2PService p2PService, PubKeyRing pubKeyRing, User user, BtcWalletService btcWalletService, OfferUtil offerUtil) { this.txFeeEstimationService = txFeeEstimationService; - 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; 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 a153cd84a0e..9ad09df365a 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,9 +41,7 @@ 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; @@ -89,9 +87,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im final TakeOfferDataModel dataModel; 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; @@ -138,7 +134,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private ChangeListener offerStateListener; private ChangeListener offerErrorListener; private ConnectionListener connectionListener; - // private Subscription isFeeSufficientSubscription; private Runnable takeOfferSucceededHandler; String marketPriceMargin; @@ -150,8 +145,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, - Preferences preferences, - PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, @@ -162,8 +155,6 @@ public TakeOfferViewModel(TakeOfferDataModel dataModel, this.btcValidator = btcValidator; this.p2PService = p2PService; - this.preferences = preferences; - this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.btcFormatter = btcFormatter; From d74719fc31888a77bac3598d868cb7b3722ee6b6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 17 Oct 2020 16:54:21 -0300 Subject: [PATCH 08/13] Move utility logic from desktop offer models to OfferUtil This is the second prelimiary refactoring to prepare for a new core.api takeoffer impl. This effort is restrained by the desktop models' dependency on JFX properties, which could not be passed as arguments to the core OfferUtil. Changes include: - Moving utility logic into OfferUtil from OfferDataModel and MutableOfferDataModel. - Injecting OfferUtil into TakeOfferDataModel, for parent OfferDataModel. - Removing unused TradeManager from OfferBook. - Moving 'isHalCashAccount' acct type check to abstract PaymentAccount. - Moving 'getTradeCurrency' logic to abstract PaymentAccount from MutableOfferDataModel's 'setTradeCurrencyFromPaymentAccount', and changing affected classes: MutableOfferView, MutableOfferViewModel. --- .../java/bisq/core/api/CoreOffersService.java | 6 +- .../main/java/bisq/core/offer/OfferUtil.java | 139 +++++++++++++----- .../bisq/core/payment/PaymentAccount.java | 24 +++ .../main/offer/MutableOfferDataModel.java | 71 +++------ .../desktop/main/offer/MutableOfferView.java | 2 +- .../main/offer/MutableOfferViewModel.java | 6 +- .../desktop/main/offer/OfferDataModel.java | 26 ++-- .../main/offer/offerbook/OfferBook.java | 7 +- .../offer/takeoffer/TakeOfferDataModel.java | 4 +- 9 files changed, 166 insertions(+), 119 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index da07677f1b2..4ed8075a509 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -22,6 +22,7 @@ import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; import bisq.core.user.User; @@ -52,16 +53,19 @@ @Slf4j class CoreOffersService { + private final OfferUtil offerUtil; private final CreateOfferService createOfferService; private final OfferBookService offerBookService; private final OpenOfferManager openOfferManager; private final User user; @Inject - public CoreOffersService(CreateOfferService createOfferService, + public CoreOffersService(OfferUtil offerUtil, + CreateOfferService createOfferService, OfferBookService offerBookService, OpenOfferManager openOfferManager, User user) { + this.offerUtil = offerUtil; this.createOfferService = createOfferService; this.offerBookService = offerBookService; this.openOfferManager = openOfferManager; diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 615631fbb58..42bce4dfb14 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -19,10 +19,10 @@ 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; +import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.payment.F2FAccount; @@ -33,12 +33,14 @@ import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.AutoConfirmSettings; import bisq.core.user.Preferences; +import bisq.core.user.User; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; import bisq.network.p2p.P2PService; import bisq.common.app.Capabilities; +import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; @@ -56,12 +58,16 @@ 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 creating and taking an Offer. + * This class holds utility methods for creating, editing and taking an Offer. */ @Slf4j @Singleton @@ -74,6 +80,7 @@ public class OfferUtil { private final PriceFeedService priceFeedService; private final P2PService p2PService; private final ReferralIdService referralIdService; + private final User user; @Inject public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, @@ -82,7 +89,8 @@ public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, Preferences preferences, PriceFeedService priceFeedService, P2PService p2PService, - ReferralIdService referralIdService) { + ReferralIdService referralIdService, + User user) { this.accountAgeWitnessService = accountAgeWitnessService; this.bsqWalletService = bsqWalletService; this.filterManager = filterManager; @@ -90,6 +98,7 @@ public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, this.priceFeedService = priceFeedService; this.p2PService = p2PService; this.referralIdService = referralIdService; + this.user = user; } /** @@ -103,6 +112,56 @@ public boolean isBuyOffer(Direction direction) { return direction == Direction.BUY; } + + /** + * Return true if a balance can cover a cost. + * + * @param cost the cost of a trade + * @param balance a wallet balance + * @return true if balance >= cost + */ + public boolean isBalanceSufficient(Coin cost, Coin balance) { + return cost != null && balance.compareTo(cost) >= 0; + } + + /** + * Return the wallet balance shortage for a given trade cost, or zero if there is + * no shortage. + * + * @param cost the cost of a trade + * @param balance a wallet balance + * @return the wallet balance shortage for the given cost, else zero. + */ + public Coin getBalanceShortage(Coin cost, Coin balance) { + if (cost != null) { + Coin shortage = cost.subtract(balance); + return shortage.isNegative() ? Coin.ZERO : shortage; + } else { + return Coin.ZERO; + } + } + + /** + * 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. * @@ -147,7 +206,7 @@ public boolean isBsqForMakerFeeAvailable(@Nullable Coin amount) { 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(); @@ -181,7 +240,7 @@ public boolean isBsqForTakerFeeAvailable(@Nullable Coin amount) { 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(); @@ -198,37 +257,6 @@ public Optional getFeeInUserFiatCurrency(Coin makerFee, bsqFormatter); } - 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); - - 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 Map getExtraDataMap(PaymentAccount paymentAccount, String currencyCode, Direction direction) { @@ -266,15 +294,46 @@ public void validateOfferData(double buyerSecurityDeposit, 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")); } + + 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); + + 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..f6e97fe4a77 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -174,6 +174,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/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index b869b1aa83c..38a9f130dcc 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -33,13 +33,11 @@ import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; -import bisq.core.util.VolumeUtil; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; 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; @@ -49,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; @@ -65,6 +64,8 @@ import javax.inject.Named; +import com.google.common.base.Predicate; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -103,7 +104,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs final String shortOfferId; private final AccountAgeWitnessService accountAgeWitnessService; private final FeeService feeService; - protected final OfferUtil offerUtil; private final CoinFormatter btcFormatter; private final Navigation navigation; private final String offerId; @@ -134,6 +134,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 @@ -154,7 +157,7 @@ public MutableOfferDataModel(CreateOfferService createOfferService, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, TradeStatisticsManager tradeStatisticsManager, Navigation navigation) { - super(btcWalletService); + super(btcWalletService, offerUtil); this.createOfferService = createOfferService; this.openOfferManager = openOfferManager; @@ -165,7 +168,6 @@ public MutableOfferDataModel(CreateOfferService createOfferService, this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.feeService = feeService; - this.offerUtil = offerUtil; this.btcFormatter = btcFormatter; this.navigation = navigation; this.tradeStatisticsManager = tradeStatisticsManager; @@ -244,7 +246,6 @@ public boolean initWithData(OfferPayload.Direction direction, TradeCurrency trad Optional paymentAccountOptional = paymentAccounts.stream().findAny(); if (paymentAccountOptional.isPresent()) { this.paymentAccount = paymentAccountOptional.get(); - } else { log.warn("PaymentAccount not available. Should never get called as in offer view you should not be able to open a create offer view"); return false; @@ -374,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()); @@ -513,43 +507,30 @@ 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); - return manualPriceAsDouble; } 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); - volume.set(volumeByAmount); - calculateMinVolume(); } catch (Throwable t) { log.error(t.toString()); } } - updateBalance(); } void calculateMinVolume() { - if (price.get() != null && - minAmount.get() != null && - !minAmount.get().isZero() && - !price.get().isZero()) { + if (isPositivePrice.test(price) && isPositiveAmount.test(amount)) { try { Volume volumeByAmount = calculateVolumeForAmount(minAmount); - minVolume.set(volumeByAmount); - } catch (Throwable t) { log.error(t.toString()); } @@ -560,22 +541,19 @@ private Volume calculateVolumeForAmount(ObjectProperty minAmount) { Volume volumeByAmount = price.get().getVolumeByAmount(minAmount.get()); // For HalCash we want multiple of 10 EUR - if (isHalCashAccount()) + if (paymentAccount.isHalCashAccount()) volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) 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()) + if (paymentAccount.isHalCashAccount()) value = CoinUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit()); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) value = CoinUtil.getRoundedFiatAmount(value, price.get(), getMaxTradeLimit()); @@ -599,6 +577,7 @@ void calculateTotalToPay() { Coin feeAndSecDeposit = getTxFee().add(getSecurityDeposit()); if (isCurrencyForMakerFeeBtc()) feeAndSecDeposit = feeAndSecDeposit.add(makerFee); + Coin total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get()); totalToPayAsCoin.set(total); updateBalance(); @@ -646,7 +625,7 @@ void setBuyerSecurityDeposit(double value) { } protected boolean isUseMarketBasedPriceValue() { - return marketPriceAvailable && useMarketBasedPrice.get() && !isHalCashAccount(); + return marketPriceAvailable && useMarketBasedPrice.get() && !paymentAccount.isHalCashAccount(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -721,13 +700,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) { @@ -762,10 +735,6 @@ boolean isBsqForFeeAvailable() { return offerUtil.isBsqForMakerFeeAvailable(amount.get()); } - public boolean isHalCashAccount() { - return paymentAccount instanceof HalCashAccount; - } - boolean canPlaceOffer() { return GUIUtil.isBootstrappedOrShowPopup(p2PService) && GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); 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 3cb5aa7b562..fb61e89d680 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -854,7 +854,7 @@ 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()) + if (dataModel.paymentAccount.isHalCashAccount()) volume = VolumeUtil.getAdjustedVolumeForHalCash(volume); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) volume = VolumeUtil.getRoundedFiatVolume(volume); @@ -1091,7 +1091,7 @@ private void setAmountToModel() { long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); if (price != null) { - if (dataModel.isHalCashAccount()) + if (dataModel.paymentAccount.isHalCashAccount()) amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) amount = CoinUtil.getRoundedFiatAmount(amount, price, maxTradeLimit); @@ -1115,7 +1115,7 @@ private void setMinAmountToModel() { Price price = dataModel.getPrice().get(); long maxTradeLimit = dataModel.getMaxTradeLimit(); if (price != null) { - if (dataModel.isHalCashAccount()) + if (dataModel.paymentAccount.isHalCashAccount()) minAmount = CoinUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit); else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get())) minAmount = CoinUtil.getRoundedFiatAmount(minAmount, price, maxTradeLimit); 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..eb5a806aa0f 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,6 +32,8 @@ 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). @@ -38,6 +41,7 @@ */ public abstract class OfferDataModel extends ActivatableDataModel { protected final BtcWalletService btcWalletService; + protected final OfferUtil offerUtil; @Getter protected final BooleanProperty isBtcWalletFunded = new SimpleBooleanProperty(); @@ -54,8 +58,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 +69,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/offerbook/OfferBook.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java index b6108d4ef35..ceac8ced769 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java @@ -20,7 +20,6 @@ import bisq.core.filter.FilterManager; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; -import bisq.core.trade.TradeManager; import javax.inject.Inject; import javax.inject.Singleton; @@ -58,7 +57,7 @@ public class OfferBook { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - OfferBook(OfferBookService offerBookService, TradeManager tradeManager, FilterManager filterManager) { + OfferBook(OfferBookService offerBookService, FilterManager filterManager) { this.offerBookService = offerBookService; this.filterManager = filterManager; @@ -98,12 +97,12 @@ public void onAdded(Offer offer) { @Override public void onRemoved(Offer offer) { - removeOffer(offer, tradeManager); + removeOffer(offer); } }); } - public void removeOffer(Offer offer, TradeManager tradeManager) { + public void removeOffer(Offer offer) { // Update state in case that that offer is used in the take offer screen, so it gets updated correctly offer.setState(Offer.State.REMOVED); 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 5733d69b408..2b6c181732e 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 @@ -137,7 +137,7 @@ class TakeOfferDataModel extends OfferDataModel { Navigation navigation, P2PService p2PService ) { - super(btcWalletService); + super(btcWalletService, offerUtil); this.tradeManager = tradeManager; this.offerBook = offerBook; @@ -278,7 +278,7 @@ public void onClose() { // confused to see the same offer still in the offerbook we remove it manually. This removal has // only local effect. Other trader might see the offer for a few seconds // still (but cannot take it). - offerBook.removeOffer(checkNotNull(offer), tradeManager); + offerBook.removeOffer(checkNotNull(offer)); btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } From f785b3b79509be319fd8d19863889892b332609c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 17 Oct 2020 17:20:55 -0300 Subject: [PATCH 09/13] Remove unused fields and imports --- core/src/main/java/bisq/core/api/CoreOffersService.java | 6 +----- core/src/main/java/bisq/core/offer/OfferUtil.java | 7 +------ 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 4ed8075a509..da07677f1b2 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -22,7 +22,6 @@ import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; -import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; import bisq.core.user.User; @@ -53,19 +52,16 @@ @Slf4j class CoreOffersService { - private final OfferUtil offerUtil; private final CreateOfferService createOfferService; private final OfferBookService offerBookService; private final OpenOfferManager openOfferManager; private final User user; @Inject - public CoreOffersService(OfferUtil offerUtil, - CreateOfferService createOfferService, + public CoreOffersService(CreateOfferService createOfferService, OfferBookService offerBookService, OpenOfferManager openOfferManager, User user) { - this.offerUtil = offerUtil; this.createOfferService = createOfferService; this.offerBookService = offerBookService; this.openOfferManager = openOfferManager; diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 42bce4dfb14..de301daaf6b 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -22,7 +22,6 @@ import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; -import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.payment.F2FAccount; @@ -33,7 +32,6 @@ import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.AutoConfirmSettings; import bisq.core.user.Preferences; -import bisq.core.user.User; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinUtil; @@ -80,7 +78,6 @@ public class OfferUtil { private final PriceFeedService priceFeedService; private final P2PService p2PService; private final ReferralIdService referralIdService; - private final User user; @Inject public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, @@ -89,8 +86,7 @@ public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, Preferences preferences, PriceFeedService priceFeedService, P2PService p2PService, - ReferralIdService referralIdService, - User user) { + ReferralIdService referralIdService) { this.accountAgeWitnessService = accountAgeWitnessService; this.bsqWalletService = bsqWalletService; this.filterManager = filterManager; @@ -98,7 +94,6 @@ public OfferUtil(AccountAgeWitnessService accountAgeWitnessService, this.priceFeedService = priceFeedService; this.p2PService = p2PService; this.referralIdService = referralIdService; - this.user = user; } /** From 48c851dde2b3bdf362f2a75b38db70af5ac9835b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:00:39 -0300 Subject: [PATCH 10/13] Add 'takeoffer' api method This is the first trade protocol method implementation. The changes needing the most scrutiny are in the new bisq.core.offer.takeoffer.TakeOfferModel, which duplicates logic in bisq.desktop.main.offer.takeoffer.TakeOfferDataModel that could not be moved into :core due to JFX dependencies. Changes include: - Adding new TradesService to grpc.proto. - Adding new CoreTradeService and GrpcOffersService. - Adding two utility methods to TakeOfferModel (instead of including them in TakeOfferModel). - Adding light proto wrapper bisq.core.api.model.TradeInfo (for CLI). - Moving static OfferInfo toOfferInfo from GrpcOffersService to OfferInfo. - Adding new CoreTradeService - Adding 'takeoffer' method to CliMain. - Adding new TakeOffer test and some refactoring of offer test classes. --- .../java/bisq/apitest/method/MethodTest.java | 16 + ...eOfferTest.java => AbstractOfferTest.java} | 58 +++- .../offer/CreateOfferUsingFixedPriceTest.java | 2 +- ...CreateOfferUsingMarketPriceMarginTest.java | 2 +- .../apitest/method/offer/TakeOfferTest.java | 91 ++++++ .../method/offer/ValidateCreateOfferTest.java | 2 +- 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 | 98 ++++++ .../java/bisq/core/api/model/OfferInfo.java | 41 ++- .../java/bisq/core/api/model/TradeInfo.java | 214 +++++++++++++ .../main/java/bisq/core/offer/OfferUtil.java | 13 + .../core/offer/takeoffer/TakeOfferModel.java | 288 ++++++++++++++++++ .../bisq/daemon/grpc/GrpcOffersService.java | 29 +- .../java/bisq/daemon/grpc/GrpcServer.java | 4 +- .../bisq/daemon/grpc/GrpcTradesService.java | 87 ++++++ proto/src/main/proto/grpc.proto | 47 ++- 18 files changed, 1000 insertions(+), 38 deletions(-) rename apitest/src/test/java/bisq/apitest/method/offer/{AbstractCreateOfferTest.java => AbstractOfferTest.java} (59%) create mode 100644 apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.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/method/MethodTest.java b/apitest/src/test/java/bisq/apitest/method/MethodTest.java index b03f00da356..d033e3e7173 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -22,12 +22,15 @@ 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 +91,14 @@ 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(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. protected final long getBalance(BisqAppConfig bisqAppConfig) { @@ -149,6 +160,11 @@ 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(); + } + // 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 59% 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..2d1ff226332 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractCreateOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -19,8 +19,12 @@ import bisq.core.monetary.Altcoin; +import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.OfferInfo; +import bisq.proto.grpc.TradeInfo; + +import protobuf.PaymentAccount; import org.bitcoinj.utils.Fiat; @@ -36,9 +40,12 @@ 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; @@ -51,11 +58,11 @@ import bisq.apitest.method.MethodTest; import bisq.cli.GrpcStubs; - @Slf4j -abstract class AbstractCreateOfferTest extends MethodTest { +abstract class AbstractOfferTest extends MethodTest { protected static GrpcStubs aliceStubs; + protected static GrpcStubs bobStubs; @BeforeAll public static void setUp() { @@ -64,9 +71,11 @@ 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. @@ -77,6 +86,39 @@ static void startSupportingApps() { } } + 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(); } @@ -103,6 +145,14 @@ protected final List sortOffersByDate(List offerInfoList) .collect(Collectors.toList()); } + 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 double getScaledOfferPrice(double offerPrice, String currencyCode) { int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; return scaleDownByPowerOf10(offerPrice, precision); 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/TakeOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.java new file mode 100644 index 00000000000..229fe5bd801 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.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.apitest.method.offer; + +import bisq.proto.grpc.OfferInfo; + +import io.grpc.StatusRuntimeException; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +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.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; +import static bisq.core.trade.Trade.State.SELLER_PUBLISHED_DEPOSIT_TX; +import static bisq.core.trade.Trade.TradePeriodState.FIRST_HALF; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class TakeOfferTest extends AbstractOfferTest { + + @Test + @Order(1) + public void testTakeAlicesBuyOffer() { + try { + // Alice is buyer, Bob is seller. + var aliceAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); + var aliceOffer = createAliceOffer(aliceAccount, "buy", "usd", 12500000); + var offerId = aliceOffer.getId(); + + // Wait for Alice's AddToOfferBook task. + // Wait times vary; my logs show >= 2 second delay. + sleep(2250); + List alicesOpenOffers = getOffersSortedByDate("buy", "usd"); + assertEquals(1, alicesOpenOffers.size()); + + var bobAccount = getDefaultPerfectDummyPaymentAccount(bobdaemon); + var trade = takeAlicesOffer(offerId, bobAccount.getId()); + assertNotNull(trade); + assertEquals(offerId, trade.getTradeId()); + + bitcoinCli.generateBlocks(1); + sleep(2250); + + alicesOpenOffers = getOffersSortedByDate("buy", "usd"); + assertEquals(0, alicesOpenOffers.size()); + + trade = getTrade(bobdaemon, trade.getTradeId()); + assertEquals(SELLER_PUBLISHED_DEPOSIT_TX.name(), trade.getState()); + assertEquals(DEPOSIT_PUBLISHED.name(), trade.getPhase()); + assertEquals(FIRST_HALF.name(), trade.getTradePeriodState()); + + bitcoinCli.generateBlocks(1); + sleep(2250); + + trade = getTrade(bobdaemon, trade.getTradeId()); + assertEquals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name(), trade.getState()); + assertEquals(DEPOSIT_CONFIRMED.name(), trade.getPhase()); + assertEquals(FIRST_HALF.name(), trade.getTradePeriodState()); + + } catch (StatusRuntimeException e) { + fail(e); + } + } +} 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/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 9b73e560283..8f157e367bb 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 7fe651c68be..8877a7c4209 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.TradeStatistics2; 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..c92f829e6e6 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -0,0 +1,98 @@ +/* + * 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.OfferBookService; +import bisq.core.offer.takeoffer.TakeOfferModel; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.protocol.TradeProtocol; +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 OfferBookService offerBookService; + private final TakeOfferModel takeOfferModel; + private final TradeManager tradeManager; + private final User user; + + @Inject + public CoreTradesService(OfferBookService offerBookService, + TakeOfferModel takeOfferModel, TradeManager tradeManager, + User user) { + this.offerBookService = offerBookService; + this.takeOfferModel = takeOfferModel; + this.tradeManager = tradeManager; + this.user = user; + } + + void takeOffer(Offer offer, + String paymentAccountId, + Consumer resultHandler) { + log.info("Get offer with id {}", offer.getId()); + 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(takeOfferModel.toString()); + //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))); + } + + @SuppressWarnings("unused") + private TradeProtocol getTradeProtocol(String tradeId) { + return tradeManager.getTradeProtocol(getTradeWithId(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..eaa1e6b9138 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,6 +157,7 @@ public static class OfferInfoBuilder { private String baseCurrencyCode; private String counterCurrencyCode; private long date; + private String state; public OfferInfoBuilder() { } @@ -208,6 +242,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..73c8a5c71ca --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -0,0 +1,214 @@ +/* + * 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.*; + +@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() { + } + + 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/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index de301daaf6b..13f52c454a7 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -170,6 +170,19 @@ public Coin getMakerFee(@Nullable Coin 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. 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..aca554a152d --- /dev/null +++ b/core/src/main/java/bisq/core/offer/takeoffer/TakeOfferModel.java @@ -0,0 +1,288 @@ +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; + +@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 = Coin.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 = Coin.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() { + return securityDeposit + .add(getTxFeeForDepositTx()) + .add(getTxFeeForPayoutTx() + .add(amount)); + } + + 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 = Coin.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..0db4e4cb525 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -43,14 +43,16 @@ public GrpcServer(Config config, GrpcOffersService offersService, GrpcPaymentAccountsService paymentAccountsService, GrpcPriceService priceService, - GrpcVersionService versionService, + GrpcTradesService tradesService, GrpcGetTradeStatisticsService tradeStatisticsService, + GrpcVersionService versionService, GrpcWalletsService walletsService) { this.server = ServerBuilder.forPort(config.apiPort) .addService(disputeAgentsService) .addService(offersService) .addService(paymentAccountsService) .addService(priceService) + .addService(tradesService) .addService(tradeStatisticsService) .addService(versionService) .addService(walletsService) 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 ca84e3ff714..3ba03421a98 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,50 @@ message GetTradeStatisticsReply { repeated TradeStatistics2 TradeStatistics = 1; } + +/////////////////////////////////////////////////////////////////////////////////////////// +// Trades +/////////////////////////////////////////////////////////////////////////////////////////// + +service Trades { + rpc GetTrade (GetTradeRequest) returns (GetTradeReply) { + } + rpc TakeOffer (TakeOfferRequest) returns (TakeOfferReply) { + } +} + +message TakeOfferRequest { + string offerId = 1; + string paymentAccountId = 2; +} + +message TakeOfferReply { + TradeInfo trade = 1; +} + +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 da0d442360dc1e4fddb64705296389a5763ce618 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:15:37 -0300 Subject: [PATCH 11/13] Remove unused field --- core/src/main/java/bisq/core/api/CoreTradesService.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index c92f829e6e6..3887fd5523e 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -18,7 +18,6 @@ package bisq.core.api; import bisq.core.offer.Offer; -import bisq.core.offer.OfferBookService; import bisq.core.offer.takeoffer.TakeOfferModel; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; @@ -36,16 +35,14 @@ @Slf4j class CoreTradesService { - private final OfferBookService offerBookService; private final TakeOfferModel takeOfferModel; private final TradeManager tradeManager; private final User user; @Inject - public CoreTradesService(OfferBookService offerBookService, - TakeOfferModel takeOfferModel, TradeManager tradeManager, + public CoreTradesService(TakeOfferModel takeOfferModel, + TradeManager tradeManager, User user) { - this.offerBookService = offerBookService; this.takeOfferModel = takeOfferModel; this.tradeManager = tradeManager; this.user = user; From 341ac89ffe0df6b72add478499c598308fcf9c15 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:15:51 -0300 Subject: [PATCH 12/13] Remove default constructors for codacy --- core/src/main/java/bisq/core/api/model/OfferInfo.java | 3 --- core/src/main/java/bisq/core/api/model/TradeInfo.java | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) 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 eaa1e6b9138..ad2389e438a 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -159,9 +159,6 @@ public static class OfferInfoBuilder { private long date; private String state; - public OfferInfoBuilder() { - } - public OfferInfoBuilder withId(String id) { this.id = id; return 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 index 73c8a5c71ca..33b8059f9b5 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -24,7 +24,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; -import static bisq.core.api.model.OfferInfo.*; +import static bisq.core.api.model.OfferInfo.toOfferInfo; @EqualsAndHashCode @Getter @@ -126,9 +126,6 @@ public static class TradeInfoBuilder { private boolean isPayoutPublished; private boolean isWithdrawn; - public TradeInfoBuilder() { - } - public TradeInfoBuilder withOffer(OfferInfo offer) { this.offer = offer; return this; From 5a55c8e9a48cf402182677427453b24c07ba8c01 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 19 Oct 2020 12:54:29 -0300 Subject: [PATCH 13/13] Add 'confirmpaymentsent' api method - Add ConfirmPaymentStarted rpc to grpc.proto. - Add new method to GrpcTradesService, CoreTradesService, CoreApi, & CliMain. - Refactor api offer/trade test hierarchy, and add new test case. --- .../test/java/bisq/apitest/ApiTestCase.java | 7 +- .../java/bisq/apitest/method/MethodTest.java | 10 ++ .../method/offer/AbstractOfferTest.java | 19 +-- .../apitest/method/offer/TakeOfferTest.java | 91 -------------- .../method/trade/AbstractTradeTest.java | 32 +++++ .../method/trade/TakeBuyBTCOfferTest.java | 116 ++++++++++++++++++ cli/src/main/java/bisq/cli/CliMain.java | 17 ++- core/src/main/java/bisq/core/api/CoreApi.java | 4 + .../java/bisq/core/api/CoreTradesService.java | 23 +++- .../bisq/daemon/grpc/GrpcTradesService.java | 17 +++ proto/src/main/proto/grpc.proto | 9 ++ 11 files changed, 233 insertions(+), 112 deletions(-) delete mode 100644 apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.java 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 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 d033e3e7173..d629daab11a 100644 --- a/apitest/src/test/java/bisq/apitest/method/MethodTest.java +++ b/apitest/src/test/java/bisq/apitest/method/MethodTest.java @@ -17,6 +17,7 @@ package bisq.apitest.method; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; import bisq.proto.grpc.CreatePaymentAccountRequest; import bisq.proto.grpc.GetBalanceRequest; import bisq.proto.grpc.GetFundingAddressesRequest; @@ -99,6 +100,10 @@ protected final GetTradeRequest createGetTradeRequest(String tradeId) { return GetTradeRequest.newBuilder().setTradeId(tradeId).build(); } + protected final ConfirmPaymentStartedRequest createConfirmPaymentStartedRequest(String tradeId) { + return ConfirmPaymentStartedRequest.newBuilder().setTradeId(tradeId).build(); + } + // Convenience methods for calling frequently used & thoroughly tested gRPC services. protected final long getBalance(BisqAppConfig bisqAppConfig) { @@ -165,6 +170,11 @@ protected final TradeInfo getTrade(BisqAppConfig bisqAppConfig, String 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); + } + // 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/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index 2d1ff226332..cb45797cd39 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -22,7 +22,6 @@ import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.GetOffersRequest; import bisq.proto.grpc.OfferInfo; -import bisq.proto.grpc.TradeInfo; import protobuf.PaymentAccount; @@ -50,7 +49,6 @@ 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; @@ -59,7 +57,7 @@ import bisq.cli.GrpcStubs; @Slf4j -abstract class AbstractOfferTest extends MethodTest { +public abstract class AbstractOfferTest extends MethodTest { protected static GrpcStubs aliceStubs; protected static GrpcStubs bobStubs; @@ -79,8 +77,7 @@ static void startSupportingApps() { // 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); } @@ -131,6 +128,10 @@ protected final OfferInfo getMostRecentOffer(String direction, String currencyCo return offerInfoList.get(offerInfoList.size() - 1); } + protected final int getOpenOffersCount(String direction, String currencyCode) { + return getOffersSortedByDate(direction, currencyCode).size(); + } + protected final List getOffersSortedByDate(String direction, String currencyCode) { var req = GetOffersRequest.newBuilder() .setDirection(direction) @@ -145,14 +146,6 @@ protected final List sortOffersByDate(List offerInfoList) .collect(Collectors.toList()); } - 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 double getScaledOfferPrice(double offerPrice, String currencyCode) { int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; return scaleDownByPowerOf10(offerPrice, precision); diff --git a/apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.java deleted file mode 100644 index 229fe5bd801..00000000000 --- a/apitest/src/test/java/bisq/apitest/method/offer/TakeOfferTest.java +++ /dev/null @@ -1,91 +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.apitest.method.offer; - -import bisq.proto.grpc.OfferInfo; - -import io.grpc.StatusRuntimeException; - -import java.util.List; - -import lombok.extern.slf4j.Slf4j; - -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.State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN; -import static bisq.core.trade.Trade.State.SELLER_PUBLISHED_DEPOSIT_TX; -import static bisq.core.trade.Trade.TradePeriodState.FIRST_HALF; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; - -@Slf4j -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class TakeOfferTest extends AbstractOfferTest { - - @Test - @Order(1) - public void testTakeAlicesBuyOffer() { - try { - // Alice is buyer, Bob is seller. - var aliceAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon); - var aliceOffer = createAliceOffer(aliceAccount, "buy", "usd", 12500000); - var offerId = aliceOffer.getId(); - - // Wait for Alice's AddToOfferBook task. - // Wait times vary; my logs show >= 2 second delay. - sleep(2250); - List alicesOpenOffers = getOffersSortedByDate("buy", "usd"); - assertEquals(1, alicesOpenOffers.size()); - - var bobAccount = getDefaultPerfectDummyPaymentAccount(bobdaemon); - var trade = takeAlicesOffer(offerId, bobAccount.getId()); - assertNotNull(trade); - assertEquals(offerId, trade.getTradeId()); - - bitcoinCli.generateBlocks(1); - sleep(2250); - - alicesOpenOffers = getOffersSortedByDate("buy", "usd"); - assertEquals(0, alicesOpenOffers.size()); - - trade = getTrade(bobdaemon, trade.getTradeId()); - assertEquals(SELLER_PUBLISHED_DEPOSIT_TX.name(), trade.getState()); - assertEquals(DEPOSIT_PUBLISHED.name(), trade.getPhase()); - assertEquals(FIRST_HALF.name(), trade.getTradePeriodState()); - - bitcoinCli.generateBlocks(1); - sleep(2250); - - trade = getTrade(bobdaemon, trade.getTradeId()); - assertEquals(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.name(), trade.getState()); - assertEquals(DEPOSIT_CONFIRMED.name(), trade.getPhase()); - assertEquals(FIRST_HALF.name(), trade.getTradePeriodState()); - - } catch (StatusRuntimeException e) { - fail(e); - } - } -} 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..f5513d30b31 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -0,0 +1,116 @@ +/* + * 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.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.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 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; + +@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(2250); + assertEquals(1, getOpenOffersCount("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("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); + } + } + + @Test + @Order(2) + public void testAlicesConfirmPaymentStarted() { + try { + var trade = getTrade(alicedaemon, tradeId); + assertNotNull(trade); + + confirmPaymentStarted(alicedaemon, trade.getTradeId()); + + sleep(7000); + trade = getTrade(alicedaemon, tradeId); + log.info("Alice's Trade View #1 {}", trade); + assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState()); + verifyExpectedTradeStateAndPhase(trade, BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG, FIAT_SENT); + + } catch (StatusRuntimeException e) { + fail(e); + } + } +} diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 8f157e367bb..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, @@ -268,7 +270,19 @@ public static void run(String[] args) { .setPaymentAccountId(paymentAccountId) .build(); var reply = tradesService.takeOffer(request); - out.printf("trade %s successfully taken", reply.getTrade().getShortId()); + 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: { @@ -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 8877a7c4209..b1a31b4ffe3 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 3887fd5523e..b1e600d4e36 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -21,7 +21,7 @@ import bisq.core.offer.takeoffer.TakeOfferModel; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; -import bisq.core.trade.protocol.TradeProtocol; +import bisq.core.trade.protocol.BuyerProtocol; import bisq.core.user.User; import javax.inject.Inject; @@ -79,6 +79,22 @@ void takeOffer(Offer offer, ); } + void confirmPaymentStarted(String tradeId) { + var trade = getTradeWithId(tradeId); + var tradeProtocol = tradeManager.getTradeProtocol(trade); + if (trade.getOffer().isBuyOffer()) { + ((BuyerProtocol) tradeProtocol).onPaymentStarted( + () -> { + }, + errorMessage -> { + throw new IllegalStateException(errorMessage); + } + ); + } else { + throw new IllegalStateException("you are not the buyer and should not try to send payment"); + } + } + Trade getTrade(String tradeId) { return getTradeWithId(tradeId); } @@ -87,9 +103,4 @@ private Trade getTradeWithId(String tradeId) { return tradeManager.getTradeById(tradeId).orElseThrow(() -> new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); } - - @SuppressWarnings("unused") - private TradeProtocol getTradeProtocol(String tradeId) { - return tradeManager.getTradeProtocol(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 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; + } + } } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 3ba03421a98..bd0634a0705 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -177,6 +177,8 @@ service Trades { } rpc TakeOffer (TakeOfferRequest) returns (TakeOfferReply) { } + rpc ConfirmPaymentStarted (ConfirmPaymentStartedRequest) returns (ConfirmPaymentStartedReply) { + } } message TakeOfferRequest { @@ -188,6 +190,13 @@ message TakeOfferReply { TradeInfo trade = 1; } +message ConfirmPaymentStartedRequest { + string tradeId = 1; +} + +message ConfirmPaymentStartedReply { +} + message GetTradeRequest { string tradeId = 1; }