From c2174607f5547c8b78ba362e96eaf97e77447262 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 4 Jan 2021 12:35:29 -0500 Subject: [PATCH] Add isTakerApiUser field to OfferAvailabilityRequest Add UNCONF_TX_LIMIT_HIT and MAKER_DENIED_API_USER to AvailabilityResult enum Apply handling for api filter features --- core/src/main/java/bisq/core/api/CoreApi.java | 10 + .../java/bisq/core/api/CoreOffersService.java | 15 +- .../java/bisq/core/api/CoreTradesService.java | 2 + .../bisq/core/offer/AvailabilityResult.java | 4 +- .../java/bisq/core/offer/OfferFilter.java | 209 ++++++++++++++++++ .../bisq/core/offer/OpenOfferManager.java | 61 ++--- .../availability/OfferAvailabilityModel.java | 7 +- .../tasks/SendOfferAvailabilityRequest.java | 3 +- .../messages/OfferAvailabilityRequest.java | 11 +- .../java/bisq/core/trade/TradeManager.java | 11 +- .../resources/i18n/displayStrings.properties | 2 + .../main/offer/MutableOfferViewModel.java | 2 +- .../main/offer/offerbook/OfferBookView.java | 174 ++++++--------- .../offer/offerbook/OfferBookViewModel.java | 115 +--------- .../offer/takeoffer/TakeOfferDataModel.java | 4 +- .../offerbook/OfferBookViewModelTest.java | 22 +- proto/src/main/proto/pb.proto | 3 + 17 files changed, 395 insertions(+), 260 deletions(-) create mode 100644 core/src/main/java/bisq/core/offer/OfferFilter.java diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 4dffaee976..caa177664c 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -112,6 +112,15 @@ public class CoreApi { return coreOffersService.getOffers(direction, currencyCode); } + /** + * @param direction The offer direction + * @param currencyCode The offer currency + * @return Returns the offers which can be taken + */ + List getOffersAvailableForTaker(String direction, String currencyCode) { + return coreOffersService.getOffersAvailableForTaker(direction, currencyCode, true); + } + public void createAnPlaceOffer(String currencyCode, String directionAsString, String priceAsString, @@ -202,6 +211,7 @@ public class CoreApi { coreTradesService.takeOffer(offer, paymentAccountId, takerFeeCurrencyCode, + true, resultHandler); } diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 0764d33f07..b8a4ef8758 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.monetary.Price; import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.offer.OfferFilter; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; @@ -58,20 +59,24 @@ class CoreOffersService { private final OpenOfferManager openOfferManager; private final OfferUtil offerUtil; private final User user; + private final OfferFilter offerFilter; @Inject public CoreOffersService(CreateOfferService createOfferService, OfferBookService offerBookService, OpenOfferManager openOfferManager, OfferUtil offerUtil, - User user) { + User user, + OfferFilter offerFilter) { this.createOfferService = createOfferService; this.offerBookService = offerBookService; this.openOfferManager = openOfferManager; this.offerUtil = offerUtil; this.user = user; + this.offerFilter = offerFilter; } + // TODO should we add a check for offerFilter.canTakeOffer? Offer getOffer(String id) { return offerBookService.getOffers().stream() .filter(o -> o.getId().equals(id)) @@ -79,6 +84,8 @@ class CoreOffersService { new IllegalStateException(format("offer with id '%s' not found", id))); } + // TODO returns all offers also those which cannot be taken. Should we use the filter from + // getOffersAvailableForTaker here and remove the getOffersAvailableForTaker method? List getOffers(String direction, String currencyCode) { List offers = offerBookService.getOffers().stream() .filter(o -> { @@ -99,6 +106,12 @@ class CoreOffersService { return offers; } + List getOffersAvailableForTaker(String direction, String currencyCode, boolean isTakerApiUser) { + return getOffers(direction, currencyCode).stream() + .filter(offer -> offerFilter.canTakeOffer(offer, isTakerApiUser).isValid()) + .collect(Collectors.toList()); + } + // Create and place new offer. void createAndPlaceOffer(String currencyCode, String directionAsString, diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 10d21d6415..b4d7fcef18 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -82,6 +82,7 @@ class CoreTradesService { void takeOffer(Offer offer, String paymentAccountId, String takerFeeCurrencyCode, + boolean isTakerApiUser, Consumer resultHandler) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); @@ -108,6 +109,7 @@ class CoreTradesService { offer, paymentAccountId, useSavingsWallet, + isTakerApiUser, resultHandler::accept, errorMessage -> { log.error(errorMessage); diff --git a/core/src/main/java/bisq/core/offer/AvailabilityResult.java b/core/src/main/java/bisq/core/offer/AvailabilityResult.java index 2d3d749ff2..18e877c830 100644 --- a/core/src/main/java/bisq/core/offer/AvailabilityResult.java +++ b/core/src/main/java/bisq/core/offer/AvailabilityResult.java @@ -27,5 +27,7 @@ public enum AvailabilityResult { NO_MEDIATORS, USER_IGNORED, MISSING_MANDATORY_CAPABILITY, - NO_REFUND_AGENTS + NO_REFUND_AGENTS, + UNCONF_TX_LIMIT_HIT, + MAKER_DENIED_API_USER } diff --git a/core/src/main/java/bisq/core/offer/OfferFilter.java b/core/src/main/java/bisq/core/offer/OfferFilter.java new file mode 100644 index 0000000000..c22231de5b --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferFilter.java @@ -0,0 +1,209 @@ +/* + * 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.account.witness.AccountAgeWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.SetChangeListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class OfferFilter { + private final User user; + private final Preferences preferences; + private final FilterManager filterManager; + private final AccountAgeWitnessService accountAgeWitnessService; + private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); + private final Map myInsufficientTradeLimitCache = new HashMap<>(); + + @Inject + public OfferFilter(User user, + Preferences preferences, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService) { + this.user = user; + this.preferences = preferences; + this.filterManager = filterManager; + this.accountAgeWitnessService = accountAgeWitnessService; + + if (user != null) { + // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) c -> + myInsufficientTradeLimitCache.clear()); + } + } + + public enum Result { + VALID(true), + API_DISABLED, + HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER, + HAS_NOT_SAME_PROTOCOL_VERSION, + IS_IGNORED, + IS_OFFER_BANNED, + IS_CURRENCY_BANNED, + IS_PAYMENT_METHOD_BANNED, + IS_NODE_ADDRESS_BANNED, + REQUIRE_UPDATE_TO_NEW_VERSION, + IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT, + IS_MY_INSUFFICIENT_TRADE_LIMIT; + + @Getter + private final boolean isValid; + + Result(boolean isValid) { + this.isValid = isValid; + } + + Result() { + this(false); + } + } + + public Result canTakeOffer(Offer offer, boolean isTakerApiUser) { + if (isTakerApiUser && filterManager.getFilter() != null && filterManager.getFilter().isDisableApi()) { + return Result.API_DISABLED; + } + if (!isAnyPaymentAccountValidForOffer(offer)) { + return Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; + } + if (!hasSameProtocolVersion(offer)) { + return Result.HAS_NOT_SAME_PROTOCOL_VERSION; + } + if (isIgnored(offer)) { + return Result.IS_IGNORED; + } + if (isOfferBanned(offer)) { + return Result.IS_OFFER_BANNED; + } + if (isCurrencyBanned(offer)) { + return Result.IS_CURRENCY_BANNED; + } + if (isPaymentMethodBanned(offer)) { + return Result.IS_PAYMENT_METHOD_BANNED; + } + if (isNodeAddressBanned(offer)) { + return Result.IS_NODE_ADDRESS_BANNED; + } + if (requireUpdateToNewVersion()) { + return Result.REQUIRE_UPDATE_TO_NEW_VERSION; + } + if (isInsufficientCounterpartyTradeLimit(offer)) { + return Result.IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT; + } + if (isMyInsufficientTradeLimit(offer)) { + return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; + } + + return Result.VALID; + } + + public boolean isAnyPaymentAccountValidForOffer(Offer offer) { + return user.getPaymentAccounts() != null && + PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); + } + + public boolean hasSameProtocolVersion(Offer offer) { + return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; + } + + public boolean isIgnored(Offer offer) { + return preferences.getIgnoreTradersList().stream() + .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); + } + + public boolean isOfferBanned(Offer offer) { + return filterManager.isOfferIdBanned(offer.getId()); + } + + public boolean isCurrencyBanned(Offer offer) { + return filterManager.isCurrencyBanned(offer.getCurrencyCode()); + } + + public boolean isPaymentMethodBanned(Offer offer) { + return filterManager.isPaymentMethodBanned(offer.getPaymentMethod()); + } + + public boolean isNodeAddressBanned(Offer offer) { + return filterManager.isNodeAddressBanned(offer.getMakerNodeAddress()); + } + + public boolean requireUpdateToNewVersion() { + return filterManager.requireUpdateToNewVersionForTrading(); + } + + // This call is a bit expensive so we cache results + public boolean isInsufficientCounterpartyTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { + return insufficientCounterpartyTradeLimitCache.get(offerId); + } + + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), + errorMessage -> { + }); + insufficientCounterpartyTradeLimitCache.put(offerId, result); + return result; + } + + // This call is a bit expensive so we cache results + public boolean isMyInsufficientTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (myInsufficientTradeLimitCache.containsKey(offerId)) { + return myInsufficientTradeLimitCache.get(offerId); + } + + Optional accountOptional = PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, + user.getPaymentAccounts(), + accountAgeWitnessService); + long myTradeLimit = accountOptional + .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection())) + .orElse(0L); + long offerMinAmount = offer.getMinAmount().value; + log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", + accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", + Coin.valueOf(myTradeLimit).toFriendlyString(), + Coin.valueOf(offerMinAmount).toFriendlyString()); + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + accountOptional.isPresent() && + myTradeLimit < offerMinAmount; + myInsufficientTradeLimitCache.put(offerId, result); + return result; + } +} diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 3e16c78402..3f07eaa1f4 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -634,47 +634,50 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe NodeAddress refundAgentNodeAddress = null; if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); - if (openOffer.getState() == OpenOffer.State.AVAILABLE) { - Offer offer = openOffer.getOffer(); - if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { - availabilityResult = AvailabilityResult.AVAILABLE; + if (!apiUserDeniedByOffer(request)) { + if (openOffer.getState() == OpenOffer.State.AVAILABLE) { + Offer offer = openOffer.getOffer(); + if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { + mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); + openOffer.setMediatorNodeAddress(mediatorNodeAddress); - mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); - openOffer.setMediatorNodeAddress(mediatorNodeAddress); + refundAgentNodeAddress = DisputeAgentSelection.getLeastUsedRefundAgent(tradeStatisticsManager, refundAgentManager).getNodeAddress(); + openOffer.setRefundAgentNodeAddress(refundAgentNodeAddress); - refundAgentNodeAddress = DisputeAgentSelection.getLeastUsedRefundAgent(tradeStatisticsManager, refundAgentManager).getNodeAddress(); - openOffer.setRefundAgentNodeAddress(refundAgentNodeAddress); - - try { - // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference - // in trade price between the peers. Also here poor connectivity might cause market price API connection - // losses and therefore an outdated market price. - offer.checkTradePriceTolerance(request.getTakersTradePrice()); - } catch (TradePriceOutOfToleranceException e) { - log.warn("Trade price check failed because takers price is outside out tolerance."); - availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; - } catch (MarketPriceNotAvailableException e) { - log.warn(e.getMessage()); - availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; - } catch (Throwable e) { - log.warn("Trade price check failed. " + e.getMessage()); - availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + try { + // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference + // in trade price between the peers. Also here poor connectivity might cause market price API connection + // losses and therefore an outdated market price. + offer.checkTradePriceTolerance(request.getTakersTradePrice()); + availabilityResult = AvailabilityResult.AVAILABLE; + } catch (TradePriceOutOfToleranceException e) { + log.warn("Trade price check failed because takers price is outside out tolerance."); + availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; + } catch (MarketPriceNotAvailableException e) { + log.warn(e.getMessage()); + availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; + } catch (Throwable e) { + log.warn("Trade price check failed. " + e.getMessage()); + availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + } + } else { + availabilityResult = AvailabilityResult.USER_IGNORED; } } else { - availabilityResult = AvailabilityResult.USER_IGNORED; + availabilityResult = AvailabilityResult.OFFER_TAKEN; } } else { - availabilityResult = AvailabilityResult.OFFER_TAKEN; + availabilityResult = AvailabilityResult.MAKER_DENIED_API_USER; } } else { - log.warn("handleOfferAvailabilityRequest: openOffer not found. That should never happen."); + log.warn("handleOfferAvailabilityRequest: openOffer not found."); availabilityResult = AvailabilityResult.OFFER_TAKEN; } if (btcWalletService.isUnconfirmedTransactionsLimitHit() || bsqWalletService.isUnconfirmedTransactionsLimitHit()) { errorMessage = Res.get("shared.unconfirmedTransactionsLimitReached"); log.warn(errorMessage); - availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + availabilityResult = AvailabilityResult.UNCONF_TX_LIMIT_HIT; } OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, @@ -716,6 +719,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + private boolean apiUserDeniedByOffer(OfferAvailabilityRequest request) { + return preferences.isDenyApiTaker() && request.isTakerApiUser(); + } + private void sendAckMessage(OfferAvailabilityRequest message, NodeAddress sender, boolean result, diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java index 8d183d4085..c1559cec8d 100644 --- a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java @@ -66,19 +66,24 @@ public class OfferAvailabilityModel implements Model { @Getter private NodeAddress selectedRefundAgent; + // Added in v1.5.5 + @Getter + private final boolean isTakerApiUser; public OfferAvailabilityModel(Offer offer, PubKeyRing pubKeyRing, P2PService p2PService, User user, MediatorManager mediatorManager, - TradeStatisticsManager tradeStatisticsManager) { + TradeStatisticsManager tradeStatisticsManager, + boolean isTakerApiUser) { this.offer = offer; this.pubKeyRing = pubKeyRing; this.p2PService = p2PService; this.user = user; this.mediatorManager = mediatorManager; this.tradeStatisticsManager = tradeStatisticsManager; + this.isTakerApiUser = isTakerApiUser; } public NodeAddress getPeerNodeAddress() { diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java index 3ee88536c6..0dbc8e69ea 100644 --- a/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java +++ b/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -39,7 +39,8 @@ public class SendOfferAvailabilityRequest extends Task { try { runInterceptHook(); - OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(), model.getPubKeyRing(), model.getTakersTradePrice()); + OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(), + model.getPubKeyRing(), model.getTakersTradePrice(), model.isTakerApiUser()); log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getOfferId(), message.getUid(), model.getPeerNodeAddress()); diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java index a3cbd1c9d0..6d9d14eaf6 100644 --- a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java @@ -42,13 +42,16 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp private final long takersTradePrice; @Nullable private final Capabilities supportedCapabilities; + private final boolean isTakerApiUser; public OfferAvailabilityRequest(String offerId, PubKeyRing pubKeyRing, - long takersTradePrice) { + long takersTradePrice, + boolean isTakerApiUser) { this(offerId, pubKeyRing, takersTradePrice, + isTakerApiUser, Capabilities.app, Version.getP2PMessageVersion(), UUID.randomUUID().toString()); @@ -62,12 +65,14 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp private OfferAvailabilityRequest(String offerId, PubKeyRing pubKeyRing, long takersTradePrice, + boolean isTakerApiUser, @Nullable Capabilities supportedCapabilities, int messageVersion, @Nullable String uid) { super(messageVersion, offerId, uid); this.pubKeyRing = pubKeyRing; this.takersTradePrice = takersTradePrice; + this.isTakerApiUser = isTakerApiUser; this.supportedCapabilities = supportedCapabilities; } @@ -76,7 +81,8 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp final protobuf.OfferAvailabilityRequest.Builder builder = protobuf.OfferAvailabilityRequest.newBuilder() .setOfferId(offerId) .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setTakersTradePrice(takersTradePrice); + .setTakersTradePrice(takersTradePrice) + .setIsTakerApiUser(isTakerApiUser); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); @@ -90,6 +96,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp return new OfferAvailabilityRequest(proto.getOfferId(), PubKeyRing.fromProto(proto.getPubKeyRing()), proto.getTakersTradePrice(), + proto.getIsTakerApiUser(), Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion, proto.getUid().isEmpty() ? null : proto.getUid()); diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index dc87d4a98c..6707fde8ea 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -373,6 +373,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// public void checkOfferAvailability(Offer offer, + boolean isTakerApiUser, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (btcWalletService.isUnconfirmedTransactionsLimitHit() || @@ -383,7 +384,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - offer.checkOfferAvailability(getOfferAvailabilityModel(offer), resultHandler, errorMessageHandler); + offer.checkOfferAvailability(getOfferAvailabilityModel(offer, isTakerApiUser), resultHandler, errorMessageHandler); } // First we check if offer is still available then we create the trade with the protocol @@ -396,12 +397,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi Offer offer, String paymentAccountId, boolean useSavingsWallet, + boolean isTakerApiUser, TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); - OfferAvailabilityModel model = getOfferAvailabilityModel(offer); + OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser); offer.checkOfferAvailability(model, () -> { if (offer.getState() == Offer.State.AVAILABLE) { @@ -464,14 +466,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi processModelServiceProvider.getKeyRing().getPubKeyRing()); } - private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer) { + private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer, boolean isTakerApiUser) { return new OfferAvailabilityModel( offer, keyRing.getPubKeyRing(), p2PService, user, mediatorManager, - tradeStatisticsManager); + tradeStatisticsManager, + isTakerApiUser); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4af5a97364..fa72834030 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1223,6 +1223,7 @@ setting.preferences.useAnimations=Use animations setting.preferences.useDarkMode=Use dark mode setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods +setting.preferences.denyApiTaker=Deny takers using the API setting.preferences.resetAllFlags=Reset all \"Don't show again\" flags settings.preferences.languageChange=To apply the language change to all screens requires a restart. settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. @@ -2620,6 +2621,7 @@ filterWindow.disableTradeBelowVersion=Min. version required for trading filterWindow.add=Add filter filterWindow.remove=Remove filter filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API offerDetailsWindow.minBtcAmount=Min. BTC amount offerDetailsWindow.min=(min. {0}) 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 bdbb89cb33..8d435ede85 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -236,7 +236,7 @@ public abstract class MutableOfferViewModel ext if (DevEnv.isDevMode()) { UserThread.runAfter(() -> { amount.set("0.001"); - price.set("70000"); + price.set("210000"); minAmount.set(amount.get()); onFocusOutPriceAsPercentageTextField(true, false); applyMakerFee(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 6c3fa3d6c2..caa01deea4 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -52,6 +52,7 @@ import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.offer.Offer; +import bisq.core.offer.OfferFilter; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferRestrictions; import bisq.core.payment.PaymentAccount; @@ -62,6 +63,7 @@ import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; +import bisq.common.app.DevEnv; import bisq.common.config.Config; import bisq.common.util.Tuple3; @@ -617,51 +619,61 @@ public class OfferBookView extends ActivatableViewAndModel account = model.getMostMaturePaymentAccountForOffer(offer); - if (account.isPresent()) { - final long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), - offer.getCurrencyCode(), offer.getMirroredDirection()); - new Popup() - .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", - formatter.formatCoinWithCode(Coin.valueOf(tradeLimit)), - Res.get("offerbook.warning.newVersionAnnouncement"))) - .show(); - } else { - log.warn("We don't found a payment account but got called the isInsufficientTradeLimit case. That must not happen."); - } + private void onShowInfo(Offer offer, OfferFilter.Result result) { + switch (result) { + case VALID: + break; + case API_DISABLED: + DevEnv.logErrorAndThrowIfDevMode("We are in desktop and in the taker position " + + "viewing offers, so it cannot be that we got that result as we are not an API user."); + break; + case HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER: + openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"), + Res.get("offerbook.warning.noMatchingAccount.msg"), + FiatAccountsView.class, + "navigation.account"); + break; + case HAS_NOT_SAME_PROTOCOL_VERSION: + new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show(); + break; + case IS_IGNORED: + new Popup().warning(Res.get("offerbook.warning.userIgnored")).show(); + break; + case IS_OFFER_BANNED: + new Popup().warning(Res.get("offerbook.warning.offerBlocked")).show(); + break; + case IS_CURRENCY_BANNED: + new Popup().warning(Res.get("offerbook.warning.currencyBanned")).show(); + break; + case IS_PAYMENT_METHOD_BANNED: + new Popup().warning(Res.get("offerbook.warning.paymentMethodBanned")).show(); + break; + case IS_NODE_ADDRESS_BANNED: + new Popup().warning(Res.get("offerbook.warning.nodeBlocked")).show(); + break; + case REQUIRE_UPDATE_TO_NEW_VERSION: + new Popup().warning(Res.get("offerbook.warning.requireUpdateToNewVersion")).show(); + break; + case IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT: + new Popup().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show(); + break; + case IS_MY_INSUFFICIENT_TRADE_LIMIT: + Optional account = model.getMostMaturePaymentAccountForOffer(offer); + if (account.isPresent()) { + long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), + offer.getCurrencyCode(), offer.getMirroredDirection()); + new Popup() + .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", + formatter.formatCoinWithCode(Coin.valueOf(tradeLimit)), + Res.get("offerbook.warning.newVersionAnnouncement"))) + .show(); + } else { + DevEnv.logErrorAndThrowIfDevMode("We don't found a payment account but got called the " + + "isInsufficientTradeLimit case."); + } + break; + default: + break; } } @@ -1015,11 +1027,7 @@ public class OfferBookView extends ActivatableViewAndModel() { final ImageView iconView = new ImageView(); final AutoTooltipButton button = new AutoTooltipButton(); - boolean isTradable, isPaymentAccountValidForOffer, - isInsufficientCounterpartyTradeLimit, - hasSameProtocolVersion, isIgnored, isOfferBanned, isCurrencyBanned, - isPaymentMethodBanned, isNodeAddressBanned, isMyInsufficientTradeLimit, - requireUpdateToNewVersion; + OfferFilter.Result canTakeOfferResult = null; { button.setGraphic(iconView); @@ -1034,37 +1042,14 @@ public class OfferBookView extends ActivatableViewAndModel tableRow = getTableRow(); if (item != null && !empty) { - final Offer offer = item.getOffer(); + Offer offer = item.getOffer(); boolean myOffer = model.isMyOffer(offer); + if (tableRow != null) { - // this code is duplicated in model.getOffersMatchingMyAccountsPredicate but as - // we want to pass the results for displaying relevant info in popups we - // cannot simply replace it with the predicate. If there are any changes we - // need to maintain both. - isPaymentAccountValidForOffer = model.isAnyPaymentAccountValidForOffer(offer); - isInsufficientCounterpartyTradeLimit = model.isInsufficientCounterpartyTradeLimit(offer); - hasSameProtocolVersion = model.hasSameProtocolVersion(offer); - isIgnored = model.isIgnored(offer); - isOfferBanned = model.isOfferBanned(offer); - isCurrencyBanned = model.isCurrencyBanned(offer); - isPaymentMethodBanned = model.isPaymentMethodBanned(offer); - isNodeAddressBanned = model.isNodeAddressBanned(offer); - requireUpdateToNewVersion = model.requireUpdateToNewVersion(); - isMyInsufficientTradeLimit = model.isMyInsufficientTradeLimit(offer); - isTradable = isPaymentAccountValidForOffer && - !isInsufficientCounterpartyTradeLimit && - hasSameProtocolVersion && - !isIgnored && - !isOfferBanned && - !isCurrencyBanned && - !isPaymentMethodBanned && - !isNodeAddressBanned && - !requireUpdateToNewVersion && - !isMyInsufficientTradeLimit; + canTakeOfferResult = model.offerFilter.canTakeOffer(offer, false); + tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4); - tableRow.setOpacity(isTradable || myOffer ? 1 : 0.4); - - if (isTradable) { + if (canTakeOfferResult.isValid()) { // set first row button as default button.setDefaultButton(getIndex() == 0); tableRow.setOnMousePressed(null); @@ -1073,17 +1058,7 @@ public class OfferBookView extends ActivatableViewAndModel { // ugly hack to get the icon clickable when deactivated if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) - onShowInfo(offer, - isPaymentAccountValidForOffer, - isInsufficientCounterpartyTradeLimit, - hasSameProtocolVersion, - isIgnored, - isOfferBanned, - isCurrencyBanned, - isPaymentMethodBanned, - isNodeAddressBanned, - requireUpdateToNewVersion, - isMyInsufficientTradeLimit); + onShowInfo(offer, canTakeOfferResult); }); } } @@ -1113,18 +1088,15 @@ public class OfferBookView extends ActivatableViewAndModel onTakeOffer(offer)); } - if (!myOffer && !isTradable) - button.setOnAction(e -> onShowInfo(offer, - isPaymentAccountValidForOffer, - isInsufficientCounterpartyTradeLimit, - hasSameProtocolVersion, - isIgnored, - isOfferBanned, - isCurrencyBanned, - isPaymentMethodBanned, - isNodeAddressBanned, - requireUpdateToNewVersion, - isMyInsufficientTradeLimit)); + if (!myOffer) { + if (canTakeOfferResult == null) { + canTakeOfferResult = model.offerFilter.canTakeOffer(offer, false); + } + + if (!canTakeOfferResult.isValid()) { + button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult)); + } + } button.updateText(title); setPadding(new Insets(0, 15, 0, 0)); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index 2a69f863fc..6e0ddd0fa3 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -28,7 +28,6 @@ import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.setup.WalletsSetup; -import bisq.core.filter.FilterManager; import bisq.core.locale.BankUtil; import bisq.core.locale.CountryUtil; import bisq.core.locale.CryptoCurrency; @@ -39,6 +38,7 @@ import bisq.core.locale.TradeCurrency; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; +import bisq.core.offer.OfferFilter; import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; @@ -56,7 +56,6 @@ import bisq.core.util.coin.CoinFormatter; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; -import bisq.common.app.Version; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; @@ -78,14 +77,12 @@ import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.collections.SetChangeListener; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import java.text.DecimalFormat; import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -105,10 +102,10 @@ class OfferBookViewModel extends ActivatableViewModel { private final P2PService p2PService; final PriceFeedService priceFeedService; private final ClosedTradableManager closedTradableManager; - private final FilterManager filterManager; final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final PriceUtil priceUtil; + final OfferFilter offerFilter; private final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; @@ -136,8 +133,6 @@ class OfferBookViewModel extends ActivatableViewModel { final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; boolean useOffersMatchingMyAccountsFilter; - private final Map myInsufficientTradeLimitCache = new HashMap<>(); - private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -153,10 +148,10 @@ class OfferBookViewModel extends ActivatableViewModel { P2PService p2PService, PriceFeedService priceFeedService, ClosedTradableManager closedTradableManager, - FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, PriceUtil priceUtil, + OfferFilter offerFilter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, BsqFormatter bsqFormatter) { super(); @@ -169,10 +164,10 @@ class OfferBookViewModel extends ActivatableViewModel { this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.closedTradableManager = closedTradableManager; - this.filterManager = filterManager; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.priceUtil = priceUtil; + this.offerFilter = offerFilter; this.btcFormatter = btcFormatter; this.bsqFormatter = bsqFormatter; @@ -213,12 +208,6 @@ class OfferBookViewModel extends ActivatableViewModel { highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMargin(offerBookListItem.getOffer(), false).length())); }; - - // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data - if (user != null) { - user.getPaymentAccountsAsObservable().addListener((SetChangeListener) c -> - myInsufficientTradeLimitCache.clear()); - } } @Override @@ -568,11 +557,6 @@ class OfferBookViewModel extends ActivatableViewModel { // Checks /////////////////////////////////////////////////////////////////////////////////////////// - boolean isAnyPaymentAccountValidForOffer(Offer offer) { - return user.getPaymentAccounts() != null && - PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); - } - boolean hasPaymentAccountForCurrency() { return (showAllTradeCurrenciesProperty.get() && user.getPaymentAccounts() != null && @@ -615,98 +599,11 @@ class OfferBookViewModel extends ActivatableViewModel { // This code duplicates code in the view at the button column. We need there the different results for // display in popups so we cannot replace that with the predicate. Any change need to be applied in both // places. - return offerBookListItem -> { - Offer offer = offerBookListItem.getOffer(); - boolean isPaymentAccountValidForOffer = isAnyPaymentAccountValidForOffer(offer); - boolean isInsufficientCounterpartyTradeLimit = isInsufficientCounterpartyTradeLimit(offer); - boolean hasSameProtocolVersion = hasSameProtocolVersion(offer); - boolean isIgnored = isIgnored(offer); - boolean isOfferBanned = isOfferBanned(offer); - boolean isCurrencyBanned = isCurrencyBanned(offer); - boolean isPaymentMethodBanned = isPaymentMethodBanned(offer); - boolean isNodeAddressBanned = isNodeAddressBanned(offer); - boolean requireUpdateToNewVersion = requireUpdateToNewVersion(); - boolean isMyInsufficientTradeLimit = isMyInsufficientTradeLimit(offer); - boolean isTradable = isPaymentAccountValidForOffer && - !isInsufficientCounterpartyTradeLimit && - hasSameProtocolVersion && - !isIgnored && - !isOfferBanned && - !isCurrencyBanned && - !isPaymentMethodBanned && - !isNodeAddressBanned && - !requireUpdateToNewVersion && - !isMyInsufficientTradeLimit; - return isTradable; - }; - } - - boolean isIgnored(Offer offer) { - return preferences.getIgnoreTradersList().stream() - .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); + return offerBookListItem -> offerFilter.canTakeOffer(offerBookListItem.getOffer(), false).isValid(); } boolean isOfferBanned(Offer offer) { - return filterManager.isOfferIdBanned(offer.getId()); - } - - boolean isCurrencyBanned(Offer offer) { - return filterManager.isCurrencyBanned(offer.getCurrencyCode()); - } - - boolean isPaymentMethodBanned(Offer offer) { - return filterManager.isPaymentMethodBanned(offer.getPaymentMethod()); - } - - boolean isNodeAddressBanned(Offer offer) { - return filterManager.isNodeAddressBanned(offer.getMakerNodeAddress()); - } - - boolean requireUpdateToNewVersion() { - return filterManager.requireUpdateToNewVersionForTrading(); - } - - // This call is a bit expensive so we cache results - boolean isInsufficientCounterpartyTradeLimit(Offer offer) { - String offerId = offer.getId(); - if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { - return insufficientCounterpartyTradeLimitCache.get(offerId); - } - - boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), - errorMessage -> { - }); - insufficientCounterpartyTradeLimitCache.put(offerId, result); - return result; - } - - // This call is a bit expensive so we cache results - boolean isMyInsufficientTradeLimit(Offer offer) { - String offerId = offer.getId(); - if (myInsufficientTradeLimitCache.containsKey(offerId)) { - return myInsufficientTradeLimitCache.get(offerId); - } - - Optional accountOptional = getMostMaturePaymentAccountForOffer(offer); - long myTradeLimit = accountOptional - .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection())) - .orElse(0L); - long offerMinAmount = offer.getMinAmount().value; - log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", - accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", - Coin.valueOf(myTradeLimit).toFriendlyString(), - Coin.valueOf(offerMinAmount).toFriendlyString()); - boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - accountOptional.isPresent() && - myTradeLimit < offerMinAmount; - myInsufficientTradeLimitCache.put(offerId, result); - return result; - } - - boolean hasSameProtocolVersion(Offer offer) { - return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; + return offerFilter.isOfferBanned(offer); } private boolean isShowAllEntry(String id) { 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 dc90ce0455..9c3eacd330 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 @@ -171,6 +171,7 @@ class TakeOfferDataModel extends OfferDataModel { if (canTakeOffer()) { tradeManager.checkOfferAvailability(offer, + false, () -> { }, errorMessage -> new Popup().warning(errorMessage).show()); @@ -319,7 +320,8 @@ class TakeOfferDataModel extends OfferDataModel { offer, paymentAccount.getId(), useSavingsWallet, - tradeResultHandler::handleResult, + false, + tradeResultHandler, errorMessage -> { log.warn(errorMessage); new Popup().warning(errorMessage).show(); diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index 39044f8d52..dc48c8c545 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -239,7 +239,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForAmount.intValue()); } @@ -253,7 +253,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); model.activate(); assertEquals(6, model.maxPlacesForAmount.intValue()); @@ -271,7 +271,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); model.activate(); assertEquals(15, model.maxPlacesForAmount.intValue()); @@ -290,7 +290,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForVolume.intValue()); } @@ -304,7 +304,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); model.activate(); assertEquals(5, model.maxPlacesForVolume.intValue()); @@ -322,7 +322,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); model.activate(); assertEquals(9, model.maxPlacesForVolume.intValue()); @@ -341,7 +341,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForPrice.intValue()); } @@ -355,7 +355,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); model.activate(); assertEquals(7, model.maxPlacesForPrice.intValue()); @@ -373,7 +373,7 @@ public class OfferBookViewModelTest { when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue()); } @@ -401,7 +401,7 @@ public class OfferBookViewModelTest { offerBookListItems.addAll(item1, item2); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, priceFeedService, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); model.activate(); assertEquals(8, model.maxPlacesForMarketPriceMargin.intValue()); //" (1.97%)" @@ -422,7 +422,7 @@ public class OfferBookViewModelTest { when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); final OfferBookListItem item = make(btcBuyItem.but( with(useMarketBasedPrice, true), diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index a5934665b6..49681858e1 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -158,6 +158,7 @@ message OfferAvailabilityRequest { int64 takers_trade_price = 3; repeated int32 supported_capabilities = 4; string uid = 5; + bool is_taker_api_user = 6; } message OfferAvailabilityResponse { @@ -914,6 +915,8 @@ enum AvailabilityResult { USER_IGNORED = 8; MISSING_MANDATORY_CAPABILITY = 9; NO_REFUND_AGENTS = 10; + UNCONF_TX_LIMIT_HIT = 11; + MAKER_DENIED_API_USER = 12; } ///////////////////////////////////////////////////////////////////////////////////////////