Add isTakerApiUser field to OfferAvailabilityRequest

Add UNCONF_TX_LIMIT_HIT and MAKER_DENIED_API_USER to AvailabilityResult enum
Apply handling for api filter features
This commit is contained in:
chimp1984 2021-01-04 12:35:29 -05:00
parent 95063b6c7f
commit c2174607f5
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
17 changed files with 395 additions and 260 deletions

View file

@ -112,6 +112,15 @@ public class CoreApi {
return coreOffersService.getOffers(direction, currencyCode); return coreOffersService.getOffers(direction, currencyCode);
} }
/**
* @param direction The offer direction
* @param currencyCode The offer currency
* @return Returns the offers which can be taken
*/
List<Offer> getOffersAvailableForTaker(String direction, String currencyCode) {
return coreOffersService.getOffersAvailableForTaker(direction, currencyCode, true);
}
public void createAnPlaceOffer(String currencyCode, public void createAnPlaceOffer(String currencyCode,
String directionAsString, String directionAsString,
String priceAsString, String priceAsString,
@ -202,6 +211,7 @@ public class CoreApi {
coreTradesService.takeOffer(offer, coreTradesService.takeOffer(offer,
paymentAccountId, paymentAccountId,
takerFeeCurrencyCode, takerFeeCurrencyCode,
true,
resultHandler); resultHandler);
} }

View file

@ -22,6 +22,7 @@ import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService; import bisq.core.offer.CreateOfferService;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferBookService; import bisq.core.offer.OfferBookService;
import bisq.core.offer.OfferFilter;
import bisq.core.offer.OfferUtil; import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
@ -58,20 +59,24 @@ class CoreOffersService {
private final OpenOfferManager openOfferManager; private final OpenOfferManager openOfferManager;
private final OfferUtil offerUtil; private final OfferUtil offerUtil;
private final User user; private final User user;
private final OfferFilter offerFilter;
@Inject @Inject
public CoreOffersService(CreateOfferService createOfferService, public CoreOffersService(CreateOfferService createOfferService,
OfferBookService offerBookService, OfferBookService offerBookService,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
OfferUtil offerUtil, OfferUtil offerUtil,
User user) { User user,
OfferFilter offerFilter) {
this.createOfferService = createOfferService; this.createOfferService = createOfferService;
this.offerBookService = offerBookService; this.offerBookService = offerBookService;
this.openOfferManager = openOfferManager; this.openOfferManager = openOfferManager;
this.offerUtil = offerUtil; this.offerUtil = offerUtil;
this.user = user; this.user = user;
this.offerFilter = offerFilter;
} }
// TODO should we add a check for offerFilter.canTakeOffer?
Offer getOffer(String id) { Offer getOffer(String id) {
return offerBookService.getOffers().stream() return offerBookService.getOffers().stream()
.filter(o -> o.getId().equals(id)) .filter(o -> o.getId().equals(id))
@ -79,6 +84,8 @@ class CoreOffersService {
new IllegalStateException(format("offer with id '%s' not found", id))); 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<Offer> getOffers(String direction, String currencyCode) { List<Offer> getOffers(String direction, String currencyCode) {
List<Offer> offers = offerBookService.getOffers().stream() List<Offer> offers = offerBookService.getOffers().stream()
.filter(o -> { .filter(o -> {
@ -99,6 +106,12 @@ class CoreOffersService {
return offers; return offers;
} }
List<Offer> 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. // Create and place new offer.
void createAndPlaceOffer(String currencyCode, void createAndPlaceOffer(String currencyCode,
String directionAsString, String directionAsString,

View file

@ -82,6 +82,7 @@ class CoreTradesService {
void takeOffer(Offer offer, void takeOffer(Offer offer,
String paymentAccountId, String paymentAccountId,
String takerFeeCurrencyCode, String takerFeeCurrencyCode,
boolean isTakerApiUser,
Consumer<Trade> resultHandler) { Consumer<Trade> resultHandler) {
coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked(); coreWalletsService.verifyEncryptedWalletIsUnlocked();
@ -108,6 +109,7 @@ class CoreTradesService {
offer, offer,
paymentAccountId, paymentAccountId,
useSavingsWallet, useSavingsWallet,
isTakerApiUser,
resultHandler::accept, resultHandler::accept,
errorMessage -> { errorMessage -> {
log.error(errorMessage); log.error(errorMessage);

View file

@ -27,5 +27,7 @@ public enum AvailabilityResult {
NO_MEDIATORS, NO_MEDIATORS,
USER_IGNORED, USER_IGNORED,
MISSING_MANDATORY_CAPABILITY, MISSING_MANDATORY_CAPABILITY,
NO_REFUND_AGENTS NO_REFUND_AGENTS,
UNCONF_TX_LIMIT_HIT,
MAKER_DENIED_API_USER
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, Boolean> insufficientCounterpartyTradeLimitCache = new HashMap<>();
private final Map<String, Boolean> 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<PaymentAccount>) 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<PaymentAccount> 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;
}
}

View file

@ -634,47 +634,50 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
NodeAddress refundAgentNodeAddress = null; NodeAddress refundAgentNodeAddress = null;
if (openOfferOptional.isPresent()) { if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get(); OpenOffer openOffer = openOfferOptional.get();
if (openOffer.getState() == OpenOffer.State.AVAILABLE) { if (!apiUserDeniedByOffer(request)) {
Offer offer = openOffer.getOffer(); if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { Offer offer = openOffer.getOffer();
availabilityResult = AvailabilityResult.AVAILABLE; 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(); refundAgentNodeAddress = DisputeAgentSelection.getLeastUsedRefundAgent(tradeStatisticsManager, refundAgentManager).getNodeAddress();
openOffer.setMediatorNodeAddress(mediatorNodeAddress); openOffer.setRefundAgentNodeAddress(refundAgentNodeAddress);
refundAgentNodeAddress = DisputeAgentSelection.getLeastUsedRefundAgent(tradeStatisticsManager, refundAgentManager).getNodeAddress(); try {
openOffer.setRefundAgentNodeAddress(refundAgentNodeAddress); // 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
try { // losses and therefore an outdated market price.
// Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference offer.checkTradePriceTolerance(request.getTakersTradePrice());
// in trade price between the peers. Also here poor connectivity might cause market price API connection availabilityResult = AvailabilityResult.AVAILABLE;
// losses and therefore an outdated market price. } catch (TradePriceOutOfToleranceException e) {
offer.checkTradePriceTolerance(request.getTakersTradePrice()); log.warn("Trade price check failed because takers price is outside out tolerance.");
} catch (TradePriceOutOfToleranceException e) { availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE;
log.warn("Trade price check failed because takers price is outside out tolerance."); } catch (MarketPriceNotAvailableException e) {
availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; log.warn(e.getMessage());
} catch (MarketPriceNotAvailableException e) { availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE;
log.warn(e.getMessage()); } catch (Throwable e) {
availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; log.warn("Trade price check failed. " + e.getMessage());
} catch (Throwable e) { availabilityResult = AvailabilityResult.UNKNOWN_FAILURE;
log.warn("Trade price check failed. " + e.getMessage()); }
availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; } else {
availabilityResult = AvailabilityResult.USER_IGNORED;
} }
} else { } else {
availabilityResult = AvailabilityResult.USER_IGNORED; availabilityResult = AvailabilityResult.OFFER_TAKEN;
} }
} else { } else {
availabilityResult = AvailabilityResult.OFFER_TAKEN; availabilityResult = AvailabilityResult.MAKER_DENIED_API_USER;
} }
} else { } else {
log.warn("handleOfferAvailabilityRequest: openOffer not found. That should never happen."); log.warn("handleOfferAvailabilityRequest: openOffer not found.");
availabilityResult = AvailabilityResult.OFFER_TAKEN; availabilityResult = AvailabilityResult.OFFER_TAKEN;
} }
if (btcWalletService.isUnconfirmedTransactionsLimitHit() || bsqWalletService.isUnconfirmedTransactionsLimitHit()) { if (btcWalletService.isUnconfirmedTransactionsLimitHit() || bsqWalletService.isUnconfirmedTransactionsLimitHit()) {
errorMessage = Res.get("shared.unconfirmedTransactionsLimitReached"); errorMessage = Res.get("shared.unconfirmedTransactionsLimitReached");
log.warn(errorMessage); log.warn(errorMessage);
availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; availabilityResult = AvailabilityResult.UNCONF_TX_LIMIT_HIT;
} }
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, 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, private void sendAckMessage(OfferAvailabilityRequest message,
NodeAddress sender, NodeAddress sender,
boolean result, boolean result,

View file

@ -66,19 +66,24 @@ public class OfferAvailabilityModel implements Model {
@Getter @Getter
private NodeAddress selectedRefundAgent; private NodeAddress selectedRefundAgent;
// Added in v1.5.5
@Getter
private final boolean isTakerApiUser;
public OfferAvailabilityModel(Offer offer, public OfferAvailabilityModel(Offer offer,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
P2PService p2PService, P2PService p2PService,
User user, User user,
MediatorManager mediatorManager, MediatorManager mediatorManager,
TradeStatisticsManager tradeStatisticsManager) { TradeStatisticsManager tradeStatisticsManager,
boolean isTakerApiUser) {
this.offer = offer; this.offer = offer;
this.pubKeyRing = pubKeyRing; this.pubKeyRing = pubKeyRing;
this.p2PService = p2PService; this.p2PService = p2PService;
this.user = user; this.user = user;
this.mediatorManager = mediatorManager; this.mediatorManager = mediatorManager;
this.tradeStatisticsManager = tradeStatisticsManager; this.tradeStatisticsManager = tradeStatisticsManager;
this.isTakerApiUser = isTakerApiUser;
} }
public NodeAddress getPeerNodeAddress() { public NodeAddress getPeerNodeAddress() {

View file

@ -39,7 +39,8 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
try { try {
runInterceptHook(); 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 {}", log.info("Send {} with offerId {} and uid {} to peer {}",
message.getClass().getSimpleName(), message.getOfferId(), message.getClass().getSimpleName(), message.getOfferId(),
message.getUid(), model.getPeerNodeAddress()); message.getUid(), model.getPeerNodeAddress());

View file

@ -42,13 +42,16 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
private final long takersTradePrice; private final long takersTradePrice;
@Nullable @Nullable
private final Capabilities supportedCapabilities; private final Capabilities supportedCapabilities;
private final boolean isTakerApiUser;
public OfferAvailabilityRequest(String offerId, public OfferAvailabilityRequest(String offerId,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
long takersTradePrice) { long takersTradePrice,
boolean isTakerApiUser) {
this(offerId, this(offerId,
pubKeyRing, pubKeyRing,
takersTradePrice, takersTradePrice,
isTakerApiUser,
Capabilities.app, Capabilities.app,
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
UUID.randomUUID().toString()); UUID.randomUUID().toString());
@ -62,12 +65,14 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
private OfferAvailabilityRequest(String offerId, private OfferAvailabilityRequest(String offerId,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
long takersTradePrice, long takersTradePrice,
boolean isTakerApiUser,
@Nullable Capabilities supportedCapabilities, @Nullable Capabilities supportedCapabilities,
int messageVersion, int messageVersion,
@Nullable String uid) { @Nullable String uid) {
super(messageVersion, offerId, uid); super(messageVersion, offerId, uid);
this.pubKeyRing = pubKeyRing; this.pubKeyRing = pubKeyRing;
this.takersTradePrice = takersTradePrice; this.takersTradePrice = takersTradePrice;
this.isTakerApiUser = isTakerApiUser;
this.supportedCapabilities = supportedCapabilities; this.supportedCapabilities = supportedCapabilities;
} }
@ -76,7 +81,8 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
final protobuf.OfferAvailabilityRequest.Builder builder = protobuf.OfferAvailabilityRequest.newBuilder() final protobuf.OfferAvailabilityRequest.Builder builder = protobuf.OfferAvailabilityRequest.newBuilder()
.setOfferId(offerId) .setOfferId(offerId)
.setPubKeyRing(pubKeyRing.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage())
.setTakersTradePrice(takersTradePrice); .setTakersTradePrice(takersTradePrice)
.setIsTakerApiUser(isTakerApiUser);
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); 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(), return new OfferAvailabilityRequest(proto.getOfferId(),
PubKeyRing.fromProto(proto.getPubKeyRing()), PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getTakersTradePrice(), proto.getTakersTradePrice(),
proto.getIsTakerApiUser(),
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
messageVersion, messageVersion,
proto.getUid().isEmpty() ? null : proto.getUid()); proto.getUid().isEmpty() ? null : proto.getUid());

View file

@ -373,6 +373,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void checkOfferAvailability(Offer offer, public void checkOfferAvailability(Offer offer,
boolean isTakerApiUser,
ResultHandler resultHandler, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
if (btcWalletService.isUnconfirmedTransactionsLimitHit() || if (btcWalletService.isUnconfirmedTransactionsLimitHit() ||
@ -383,7 +384,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return; 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 // 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, Offer offer,
String paymentAccountId, String paymentAccountId,
boolean useSavingsWallet, boolean useSavingsWallet,
boolean isTakerApiUser,
TradeResultHandler tradeResultHandler, TradeResultHandler tradeResultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId())); checkArgument(!wasOfferAlreadyUsedInTrade(offer.getId()));
OfferAvailabilityModel model = getOfferAvailabilityModel(offer); OfferAvailabilityModel model = getOfferAvailabilityModel(offer, isTakerApiUser);
offer.checkOfferAvailability(model, offer.checkOfferAvailability(model,
() -> { () -> {
if (offer.getState() == Offer.State.AVAILABLE) { if (offer.getState() == Offer.State.AVAILABLE) {
@ -464,14 +466,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
processModelServiceProvider.getKeyRing().getPubKeyRing()); processModelServiceProvider.getKeyRing().getPubKeyRing());
} }
private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer) { private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer, boolean isTakerApiUser) {
return new OfferAvailabilityModel( return new OfferAvailabilityModel(
offer, offer,
keyRing.getPubKeyRing(), keyRing.getPubKeyRing(),
p2PService, p2PService,
user, user,
mediatorManager, mediatorManager,
tradeStatisticsManager); tradeStatisticsManager,
isTakerApiUser);
} }

View file

@ -1223,6 +1223,7 @@ setting.preferences.useAnimations=Use animations
setting.preferences.useDarkMode=Use dark mode setting.preferences.useDarkMode=Use dark mode
setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades setting.preferences.sortWithNumOffers=Sort market lists with no. of offers/trades
setting.preferences.onlyShowPaymentMethodsFromAccount=Hide non-supported payment methods 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 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.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}. 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.add=Add filter
filterWindow.remove=Remove filter filterWindow.remove=Remove filter
filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses
filterWindow.disableApi=Disable API
offerDetailsWindow.minBtcAmount=Min. BTC amount offerDetailsWindow.minBtcAmount=Min. BTC amount
offerDetailsWindow.min=(min. {0}) offerDetailsWindow.min=(min. {0})

View file

@ -236,7 +236,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
if (DevEnv.isDevMode()) { if (DevEnv.isDevMode()) {
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
amount.set("0.001"); amount.set("0.001");
price.set("70000"); price.set("210000");
minAmount.set(amount.get()); minAmount.set(amount.get());
onFocusOutPriceAsPercentageTextField(true, false); onFocusOutPriceAsPercentageTextField(true, false);
applyMakerFee(); applyMakerFee();

View file

@ -52,6 +52,7 @@ import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency; import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferFilter;
import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferRestrictions; import bisq.core.offer.OfferRestrictions;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
@ -62,6 +63,7 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.app.DevEnv;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.util.Tuple3; import bisq.common.util.Tuple3;
@ -617,51 +619,61 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
} }
} }
private void onShowInfo(Offer offer, private void onShowInfo(Offer offer, OfferFilter.Result result) {
boolean isPaymentAccountValidForOffer, switch (result) {
boolean isInsufficientCounterpartyTradeLimit, case VALID:
boolean hasSameProtocolVersion, break;
boolean isIgnored, case API_DISABLED:
boolean isOfferBanned, DevEnv.logErrorAndThrowIfDevMode("We are in desktop and in the taker position " +
boolean isCurrencyBanned, "viewing offers, so it cannot be that we got that result as we are not an API user.");
boolean isPaymentMethodBanned, break;
boolean isNodeAddressBanned, case HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER:
boolean requireUpdateToNewVersion, openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"),
boolean isInsufficientTradeLimit) { Res.get("offerbook.warning.noMatchingAccount.msg"),
if (!isPaymentAccountValidForOffer) { FiatAccountsView.class,
openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"), "navigation.account");
Res.get("offerbook.warning.noMatchingAccount.msg"), break;
FiatAccountsView.class, case HAS_NOT_SAME_PROTOCOL_VERSION:
"navigation.account"); new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show();
} else if (isInsufficientCounterpartyTradeLimit) { break;
new Popup().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show(); case IS_IGNORED:
} else if (!hasSameProtocolVersion) { new Popup().warning(Res.get("offerbook.warning.userIgnored")).show();
new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show(); break;
} else if (isIgnored) { case IS_OFFER_BANNED:
new Popup().warning(Res.get("offerbook.warning.userIgnored")).show(); new Popup().warning(Res.get("offerbook.warning.offerBlocked")).show();
} else if (isOfferBanned) { break;
new Popup().warning(Res.get("offerbook.warning.offerBlocked")).show(); case IS_CURRENCY_BANNED:
} else if (isCurrencyBanned) { new Popup().warning(Res.get("offerbook.warning.currencyBanned")).show();
new Popup().warning(Res.get("offerbook.warning.currencyBanned")).show(); break;
} else if (isPaymentMethodBanned) { case IS_PAYMENT_METHOD_BANNED:
new Popup().warning(Res.get("offerbook.warning.paymentMethodBanned")).show(); new Popup().warning(Res.get("offerbook.warning.paymentMethodBanned")).show();
} else if (isNodeAddressBanned) { break;
new Popup().warning(Res.get("offerbook.warning.nodeBlocked")).show(); case IS_NODE_ADDRESS_BANNED:
} else if (requireUpdateToNewVersion) { new Popup().warning(Res.get("offerbook.warning.nodeBlocked")).show();
new Popup().warning(Res.get("offerbook.warning.requireUpdateToNewVersion")).show(); break;
} else if (isInsufficientTradeLimit) { case REQUIRE_UPDATE_TO_NEW_VERSION:
final Optional<PaymentAccount> account = model.getMostMaturePaymentAccountForOffer(offer); new Popup().warning(Res.get("offerbook.warning.requireUpdateToNewVersion")).show();
if (account.isPresent()) { break;
final long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), case IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT:
offer.getCurrencyCode(), offer.getMirroredDirection()); new Popup().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show();
new Popup() break;
.warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", case IS_MY_INSUFFICIENT_TRADE_LIMIT:
formatter.formatCoinWithCode(Coin.valueOf(tradeLimit)), Optional<PaymentAccount> account = model.getMostMaturePaymentAccountForOffer(offer);
Res.get("offerbook.warning.newVersionAnnouncement"))) if (account.isPresent()) {
.show(); long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(),
} else { offer.getCurrencyCode(), offer.getMirroredDirection());
log.warn("We don't found a payment account but got called the isInsufficientTradeLimit case. That must not happen."); 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<GridPane, OfferBookVi
return new TableCell<>() { return new TableCell<>() {
final ImageView iconView = new ImageView(); final ImageView iconView = new ImageView();
final AutoTooltipButton button = new AutoTooltipButton(); final AutoTooltipButton button = new AutoTooltipButton();
boolean isTradable, isPaymentAccountValidForOffer, OfferFilter.Result canTakeOfferResult = null;
isInsufficientCounterpartyTradeLimit,
hasSameProtocolVersion, isIgnored, isOfferBanned, isCurrencyBanned,
isPaymentMethodBanned, isNodeAddressBanned, isMyInsufficientTradeLimit,
requireUpdateToNewVersion;
{ {
button.setGraphic(iconView); button.setGraphic(iconView);
@ -1034,37 +1042,14 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
TableRow<OfferBookListItem> tableRow = getTableRow(); TableRow<OfferBookListItem> tableRow = getTableRow();
if (item != null && !empty) { if (item != null && !empty) {
final Offer offer = item.getOffer(); Offer offer = item.getOffer();
boolean myOffer = model.isMyOffer(offer); boolean myOffer = model.isMyOffer(offer);
if (tableRow != null) { if (tableRow != null) {
// this code is duplicated in model.getOffersMatchingMyAccountsPredicate but as canTakeOfferResult = model.offerFilter.canTakeOffer(offer, false);
// we want to pass the results for displaying relevant info in popups we tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4);
// 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;
tableRow.setOpacity(isTradable || myOffer ? 1 : 0.4); if (canTakeOfferResult.isValid()) {
if (isTradable) {
// set first row button as default // set first row button as default
button.setDefaultButton(getIndex() == 0); button.setDefaultButton(getIndex() == 0);
tableRow.setOnMousePressed(null); tableRow.setOnMousePressed(null);
@ -1073,17 +1058,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
tableRow.setOnMousePressed(e -> { tableRow.setOnMousePressed(e -> {
// ugly hack to get the icon clickable when deactivated // ugly hack to get the icon clickable when deactivated
if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas))
onShowInfo(offer, onShowInfo(offer, canTakeOfferResult);
isPaymentAccountValidForOffer,
isInsufficientCounterpartyTradeLimit,
hasSameProtocolVersion,
isIgnored,
isOfferBanned,
isCurrencyBanned,
isPaymentMethodBanned,
isNodeAddressBanned,
requireUpdateToNewVersion,
isMyInsufficientTradeLimit);
}); });
} }
} }
@ -1113,18 +1088,15 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
button.setOnAction(e -> onTakeOffer(offer)); button.setOnAction(e -> onTakeOffer(offer));
} }
if (!myOffer && !isTradable) if (!myOffer) {
button.setOnAction(e -> onShowInfo(offer, if (canTakeOfferResult == null) {
isPaymentAccountValidForOffer, canTakeOfferResult = model.offerFilter.canTakeOffer(offer, false);
isInsufficientCounterpartyTradeLimit, }
hasSameProtocolVersion,
isIgnored, if (!canTakeOfferResult.isValid()) {
isOfferBanned, button.setOnAction(e -> onShowInfo(offer, canTakeOfferResult));
isCurrencyBanned, }
isPaymentMethodBanned, }
isNodeAddressBanned,
requireUpdateToNewVersion,
isMyInsufficientTradeLimit));
button.updateText(title); button.updateText(title);
setPadding(new Insets(0, 15, 0, 0)); setPadding(new Insets(0, 15, 0, 0));

View file

@ -28,7 +28,6 @@ import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.filter.FilterManager;
import bisq.core.locale.BankUtil; import bisq.core.locale.BankUtil;
import bisq.core.locale.CountryUtil; import bisq.core.locale.CountryUtil;
import bisq.core.locale.CryptoCurrency; import bisq.core.locale.CryptoCurrency;
@ -39,6 +38,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.monetary.Volume; import bisq.core.monetary.Volume;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferFilter;
import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
@ -56,7 +56,6 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.common.app.Version;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
@ -78,14 +77,12 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -105,10 +102,10 @@ class OfferBookViewModel extends ActivatableViewModel {
private final P2PService p2PService; private final P2PService p2PService;
final PriceFeedService priceFeedService; final PriceFeedService priceFeedService;
private final ClosedTradableManager closedTradableManager; private final ClosedTradableManager closedTradableManager;
private final FilterManager filterManager;
final AccountAgeWitnessService accountAgeWitnessService; final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation; private final Navigation navigation;
private final PriceUtil priceUtil; private final PriceUtil priceUtil;
final OfferFilter offerFilter;
private final CoinFormatter btcFormatter; private final CoinFormatter btcFormatter;
private final BsqFormatter bsqFormatter; private final BsqFormatter bsqFormatter;
@ -136,8 +133,6 @@ class OfferBookViewModel extends ActivatableViewModel {
final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty();
boolean showAllPaymentMethods = true; boolean showAllPaymentMethods = true;
boolean useOffersMatchingMyAccountsFilter; boolean useOffersMatchingMyAccountsFilter;
private final Map<String, Boolean> myInsufficientTradeLimitCache = new HashMap<>();
private final Map<String, Boolean> insufficientCounterpartyTradeLimitCache = new HashMap<>();
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -153,10 +148,10 @@ class OfferBookViewModel extends ActivatableViewModel {
P2PService p2PService, P2PService p2PService,
PriceFeedService priceFeedService, PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
FilterManager filterManager,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation, Navigation navigation,
PriceUtil priceUtil, PriceUtil priceUtil,
OfferFilter offerFilter,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) { BsqFormatter bsqFormatter) {
super(); super();
@ -169,10 +164,10 @@ class OfferBookViewModel extends ActivatableViewModel {
this.p2PService = p2PService; this.p2PService = p2PService;
this.priceFeedService = priceFeedService; this.priceFeedService = priceFeedService;
this.closedTradableManager = closedTradableManager; this.closedTradableManager = closedTradableManager;
this.filterManager = filterManager;
this.accountAgeWitnessService = accountAgeWitnessService; this.accountAgeWitnessService = accountAgeWitnessService;
this.navigation = navigation; this.navigation = navigation;
this.priceUtil = priceUtil; this.priceUtil = priceUtil;
this.offerFilter = offerFilter;
this.btcFormatter = btcFormatter; this.btcFormatter = btcFormatter;
this.bsqFormatter = bsqFormatter; this.bsqFormatter = bsqFormatter;
@ -213,12 +208,6 @@ class OfferBookViewModel extends ActivatableViewModel {
highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMargin(offerBookListItem.getOffer(), false).length())); 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<PaymentAccount>) c ->
myInsufficientTradeLimitCache.clear());
}
} }
@Override @Override
@ -568,11 +557,6 @@ class OfferBookViewModel extends ActivatableViewModel {
// Checks // Checks
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
boolean isAnyPaymentAccountValidForOffer(Offer offer) {
return user.getPaymentAccounts() != null &&
PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts());
}
boolean hasPaymentAccountForCurrency() { boolean hasPaymentAccountForCurrency() {
return (showAllTradeCurrenciesProperty.get() && return (showAllTradeCurrenciesProperty.get() &&
user.getPaymentAccounts() != null && 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 // 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 // display in popups so we cannot replace that with the predicate. Any change need to be applied in both
// places. // places.
return offerBookListItem -> { return offerBookListItem -> offerFilter.canTakeOffer(offerBookListItem.getOffer(), false).isValid();
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()));
} }
boolean isOfferBanned(Offer offer) { boolean isOfferBanned(Offer offer) {
return filterManager.isOfferIdBanned(offer.getId()); return offerFilter.isOfferBanned(offer);
}
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<PaymentAccount> 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;
} }
private boolean isShowAllEntry(String id) { private boolean isShowAllEntry(String id) {

View file

@ -171,6 +171,7 @@ class TakeOfferDataModel extends OfferDataModel {
if (canTakeOffer()) { if (canTakeOffer()) {
tradeManager.checkOfferAvailability(offer, tradeManager.checkOfferAvailability(offer,
false,
() -> { () -> {
}, },
errorMessage -> new Popup().warning(errorMessage).show()); errorMessage -> new Popup().warning(errorMessage).show());
@ -319,7 +320,8 @@ class TakeOfferDataModel extends OfferDataModel {
offer, offer,
paymentAccount.getId(), paymentAccount.getId(),
useSavingsWallet, useSavingsWallet,
tradeResultHandler::handleResult, false,
tradeResultHandler,
errorMessage -> { errorMessage -> {
log.warn(errorMessage); log.warn(errorMessage);
new Popup().warning(errorMessage).show(); new Popup().warning(errorMessage).show();

View file

@ -239,7 +239,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, 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()); assertEquals(0, model.maxPlacesForAmount.intValue());
} }
@ -253,7 +253,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, 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(); model.activate();
assertEquals(6, model.maxPlacesForAmount.intValue()); assertEquals(6, model.maxPlacesForAmount.intValue());
@ -271,7 +271,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, 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(); model.activate();
assertEquals(15, model.maxPlacesForAmount.intValue()); assertEquals(15, model.maxPlacesForAmount.intValue());
@ -290,7 +290,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, 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()); assertEquals(0, model.maxPlacesForVolume.intValue());
} }
@ -304,7 +304,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, 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(); model.activate();
assertEquals(5, model.maxPlacesForVolume.intValue()); assertEquals(5, model.maxPlacesForVolume.intValue());
@ -322,7 +322,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, 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(); model.activate();
assertEquals(9, model.maxPlacesForVolume.intValue()); assertEquals(9, model.maxPlacesForVolume.intValue());
@ -341,7 +341,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, 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()); assertEquals(0, model.maxPlacesForPrice.intValue());
} }
@ -355,7 +355,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, 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(); model.activate();
assertEquals(7, model.maxPlacesForPrice.intValue()); assertEquals(7, model.maxPlacesForPrice.intValue());
@ -373,7 +373,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems); when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null, 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()); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue());
} }
@ -401,7 +401,7 @@ public class OfferBookViewModelTest {
offerBookListItems.addAll(item1, item2); offerBookListItems.addAll(item1, item2);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, priceFeedService, 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(); model.activate();
assertEquals(8, model.maxPlacesForMarketPriceMargin.intValue()); //" (1.97%)" 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)); 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, 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( final OfferBookListItem item = make(btcBuyItem.but(
with(useMarketBasedPrice, true), with(useMarketBasedPrice, true),

View file

@ -158,6 +158,7 @@ message OfferAvailabilityRequest {
int64 takers_trade_price = 3; int64 takers_trade_price = 3;
repeated int32 supported_capabilities = 4; repeated int32 supported_capabilities = 4;
string uid = 5; string uid = 5;
bool is_taker_api_user = 6;
} }
message OfferAvailabilityResponse { message OfferAvailabilityResponse {
@ -914,6 +915,8 @@ enum AvailabilityResult {
USER_IGNORED = 8; USER_IGNORED = 8;
MISSING_MANDATORY_CAPABILITY = 9; MISSING_MANDATORY_CAPABILITY = 9;
NO_REFUND_AGENTS = 10; NO_REFUND_AGENTS = 10;
UNCONF_TX_LIMIT_HIT = 11;
MAKER_DENIED_API_USER = 12;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////