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);
}
/**
* @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,
String directionAsString,
String priceAsString,
@ -202,6 +211,7 @@ public class CoreApi {
coreTradesService.takeOffer(offer,
paymentAccountId,
takerFeeCurrencyCode,
true,
resultHandler);
}

View file

@ -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<Offer> getOffers(String direction, String currencyCode) {
List<Offer> offers = offerBookService.getOffers().stream()
.filter(o -> {
@ -99,6 +106,12 @@ class CoreOffersService {
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.
void createAndPlaceOffer(String currencyCode,
String directionAsString,

View file

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

View file

@ -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
}

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;
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,

View file

@ -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() {

View file

@ -39,7 +39,8 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
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());

View file

@ -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());

View file

@ -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);
}

View file

@ -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})

View file

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

View file

@ -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<GridPane, OfferBookVi
}
}
private void onShowInfo(Offer offer,
boolean isPaymentAccountValidForOffer,
boolean isInsufficientCounterpartyTradeLimit,
boolean hasSameProtocolVersion,
boolean isIgnored,
boolean isOfferBanned,
boolean isCurrencyBanned,
boolean isPaymentMethodBanned,
boolean isNodeAddressBanned,
boolean requireUpdateToNewVersion,
boolean isInsufficientTradeLimit) {
if (!isPaymentAccountValidForOffer) {
openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"),
Res.get("offerbook.warning.noMatchingAccount.msg"),
FiatAccountsView.class,
"navigation.account");
} else if (isInsufficientCounterpartyTradeLimit) {
new Popup().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show();
} else if (!hasSameProtocolVersion) {
new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show();
} else if (isIgnored) {
new Popup().warning(Res.get("offerbook.warning.userIgnored")).show();
} else if (isOfferBanned) {
new Popup().warning(Res.get("offerbook.warning.offerBlocked")).show();
} else if (isCurrencyBanned) {
new Popup().warning(Res.get("offerbook.warning.currencyBanned")).show();
} else if (isPaymentMethodBanned) {
new Popup().warning(Res.get("offerbook.warning.paymentMethodBanned")).show();
} else if (isNodeAddressBanned) {
new Popup().warning(Res.get("offerbook.warning.nodeBlocked")).show();
} else if (requireUpdateToNewVersion) {
new Popup().warning(Res.get("offerbook.warning.requireUpdateToNewVersion")).show();
} else if (isInsufficientTradeLimit) {
final Optional<PaymentAccount> 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<PaymentAccount> 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<GridPane, OfferBookVi
return new TableCell<>() {
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<GridPane, OfferBookVi
TableRow<OfferBookListItem> 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<GridPane, OfferBookVi
tableRow.setOnMousePressed(e -> {
// 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<GridPane, OfferBookVi
button.setOnAction(e -> 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));

View file

@ -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<String, Boolean> myInsufficientTradeLimitCache = new HashMap<>();
private final Map<String, Boolean> 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<PaymentAccount>) 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<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;
return offerFilter.isOfferBanned(offer);
}
private boolean isShowAllEntry(String id) {

View file

@ -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();

View file

@ -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),

View file

@ -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;
}
///////////////////////////////////////////////////////////////////////////////////////////