diff --git a/common/src/main/java/bisq/common/proto/ProtoUtil.java b/common/src/main/java/bisq/common/proto/ProtoUtil.java index e8471a4d823..d7450e9d509 100644 --- a/common/src/main/java/bisq/common/proto/ProtoUtil.java +++ b/common/src/main/java/bisq/common/proto/ProtoUtil.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -105,4 +106,7 @@ public static List protocolStringListToList(ProtocolStringList protocolS return CollectionUtils.isEmpty(protocolStringList) ? new ArrayList<>() : new ArrayList<>(protocolStringList); } + public static Set protocolStringListToSet(ProtocolStringList protocolStringList) { + return CollectionUtils.isEmpty(protocolStringList) ? new HashSet<>() : new HashSet<>(protocolStringList); + } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 4dffaee9760..caa177664ca 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -112,6 +112,15 @@ public List getOffers(String direction, String 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 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 void takeOffer(String offerId, coreTradesService.takeOffer(offer, paymentAccountId, takerFeeCurrencyCode, + true, resultHandler); } diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 0764d33f078..b8a4ef8758e 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -22,6 +22,7 @@ import bisq.core.offer.CreateOfferService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.offer.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 @@ Offer getOffer(String 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 getOffers(String direction, String currencyCode) { List offers = offerBookService.getOffers().stream() .filter(o -> { @@ -99,6 +106,12 @@ List getOffers(String direction, String currencyCode) { return offers; } + List getOffersAvailableForTaker(String direction, String currencyCode, boolean isTakerApiUser) { + return getOffers(direction, currencyCode).stream() + .filter(offer -> offerFilter.canTakeOffer(offer, isTakerApiUser).isValid()) + .collect(Collectors.toList()); + } + // Create and place new offer. void createAndPlaceOffer(String currencyCode, String directionAsString, diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 10d21d6415d..b4d7fcef188 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -82,6 +82,7 @@ public CoreTradesService(CoreWalletsService coreWalletsService, void takeOffer(Offer offer, String paymentAccountId, String takerFeeCurrencyCode, + boolean isTakerApiUser, Consumer resultHandler) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); @@ -108,6 +109,7 @@ void takeOffer(Offer offer, offer, paymentAccountId, useSavingsWallet, + isTakerApiUser, resultHandler::accept, errorMessage -> { log.error(errorMessage); diff --git a/core/src/main/java/bisq/core/app/CoreModule.java b/core/src/main/java/bisq/core/app/CoreModule.java index 8f300705323..4984a84aaee 100644 --- a/core/src/main/java/bisq/core/app/CoreModule.java +++ b/core/src/main/java/bisq/core/app/CoreModule.java @@ -21,6 +21,7 @@ import bisq.core.btc.BitcoinModule; import bisq.core.dao.DaoModule; import bisq.core.filter.FilterModule; +import bisq.core.network.CoreNetworkFilter; import bisq.core.network.p2p.seed.DefaultSeedNodeRepository; import bisq.core.offer.OfferModule; import bisq.core.presentation.CorePresentationModule; @@ -35,6 +36,7 @@ import bisq.network.crypto.EncryptionServiceModule; import bisq.network.p2p.P2PModule; import bisq.network.p2p.network.BridgeAddressProvider; +import bisq.network.p2p.network.NetworkFilter; import bisq.network.p2p.seed.SeedNodeRepository; import bisq.common.app.AppModule; @@ -44,6 +46,8 @@ import bisq.common.proto.network.NetworkProtoResolver; import bisq.common.proto.persistable.PersistenceProtoResolver; +import com.google.inject.Singleton; + import java.io.File; import static bisq.common.config.Config.*; @@ -62,6 +66,7 @@ protected void configure() { bind(BridgeAddressProvider.class).to(Preferences.class); bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class); + bind(NetworkFilter.class).to(CoreNetworkFilter.class).in(Singleton.class); bind(File.class).annotatedWith(named(STORAGE_DIR)).toInstance(config.storageDir); diff --git a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java index 4ffdb7f79cb..d4402b765dc 100644 --- a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java @@ -22,6 +22,7 @@ import bisq.core.btc.BitcoinModule; import bisq.core.dao.DaoModule; import bisq.core.filter.FilterModule; +import bisq.core.network.CoreNetworkFilter; import bisq.core.network.p2p.seed.DefaultSeedNodeRepository; import bisq.core.offer.OfferModule; import bisq.core.proto.network.CoreNetworkProtoResolver; @@ -33,6 +34,7 @@ import bisq.network.crypto.EncryptionServiceModule; import bisq.network.p2p.P2PModule; import bisq.network.p2p.network.BridgeAddressProvider; +import bisq.network.p2p.network.NetworkFilter; import bisq.network.p2p.seed.SeedNodeRepository; import bisq.common.ClockWatcher; @@ -73,6 +75,7 @@ protected void configure() { bind(TorSetup.class).in(Singleton.class); bind(SeedNodeRepository.class).to(DefaultSeedNodeRepository.class).in(Singleton.class); + bind(NetworkFilter.class).to(CoreNetworkFilter.class).in(Singleton.class); bind(File.class).annotatedWith(named(STORAGE_DIR)).toInstance(config.storageDir); bind(File.class).annotatedWith(named(KEY_STORAGE_DIR)).toInstance(config.keyStorageDir); diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index a642c9511c9..b433de9476e 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -47,7 +48,7 @@ @Value public final class Filter implements ProtectedStoragePayload, ExpirablePayload { private final List bannedOfferIds; - private final List bannedNodeAddress; + private final List nodeAddressesBannedFromTrading; private final List bannedAutoConfExplorers; private final List bannedPaymentAccounts; private final List bannedCurrencies; @@ -91,10 +92,14 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { // added at v1.3.8 private final boolean disableAutoConf; + // added at v1.5.5 + private final Set nodeAddressesBannedFromNetwork; + private final boolean disableApi; + // After we have created the signature from the filter data we clone it and apply the signature static Filter cloneWithSig(Filter filter, String signatureAsBase64) { return new Filter(filter.getBannedOfferIds(), - filter.getBannedNodeAddress(), + filter.getNodeAddressesBannedFromTrading(), filter.getBannedPaymentAccounts(), filter.getBannedCurrencies(), filter.getBannedPaymentMethods(), @@ -117,13 +122,15 @@ static Filter cloneWithSig(Filter filter, String signatureAsBase64) { filter.getSignerPubKeyAsHex(), filter.getBannedPrivilegedDevPubKeys(), filter.isDisableAutoConf(), - filter.getBannedAutoConfExplorers()); + filter.getBannedAutoConfExplorers(), + filter.getNodeAddressesBannedFromNetwork(), + filter.isDisableApi()); } // Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again static Filter cloneWithoutSig(Filter filter) { return new Filter(filter.getBannedOfferIds(), - filter.getBannedNodeAddress(), + filter.getNodeAddressesBannedFromTrading(), filter.getBannedPaymentAccounts(), filter.getBannedCurrencies(), filter.getBannedPaymentMethods(), @@ -146,11 +153,13 @@ static Filter cloneWithoutSig(Filter filter) { filter.getSignerPubKeyAsHex(), filter.getBannedPrivilegedDevPubKeys(), filter.isDisableAutoConf(), - filter.getBannedAutoConfExplorers()); + filter.getBannedAutoConfExplorers(), + filter.getNodeAddressesBannedFromNetwork(), + filter.isDisableApi()); } public Filter(List bannedOfferIds, - List bannedNodeAddress, + List nodeAddressesBannedFromTrading, List bannedPaymentAccounts, List bannedCurrencies, List bannedPaymentMethods, @@ -170,9 +179,11 @@ public Filter(List bannedOfferIds, String signerPubKeyAsHex, List bannedPrivilegedDevPubKeys, boolean disableAutoConf, - List bannedAutoConfExplorers) { + List bannedAutoConfExplorers, + Set nodeAddressesBannedFromNetwork, + boolean disableApi) { this(bannedOfferIds, - bannedNodeAddress, + nodeAddressesBannedFromTrading, bannedPaymentAccounts, bannedCurrencies, bannedPaymentMethods, @@ -195,7 +206,9 @@ public Filter(List bannedOfferIds, signerPubKeyAsHex, bannedPrivilegedDevPubKeys, disableAutoConf, - bannedAutoConfExplorers); + bannedAutoConfExplorers, + nodeAddressesBannedFromNetwork, + disableApi); } @@ -205,7 +218,7 @@ public Filter(List bannedOfferIds, @VisibleForTesting public Filter(List bannedOfferIds, - List bannedNodeAddress, + List nodeAddressesBannedFromTrading, List bannedPaymentAccounts, List bannedCurrencies, List bannedPaymentMethods, @@ -228,9 +241,11 @@ public Filter(List bannedOfferIds, String signerPubKeyAsHex, List bannedPrivilegedDevPubKeys, boolean disableAutoConf, - List bannedAutoConfExplorers) { + List bannedAutoConfExplorers, + Set nodeAddressesBannedFromNetwork, + boolean disableApi) { this.bannedOfferIds = bannedOfferIds; - this.bannedNodeAddress = bannedNodeAddress; + this.nodeAddressesBannedFromTrading = nodeAddressesBannedFromTrading; this.bannedPaymentAccounts = bannedPaymentAccounts; this.bannedCurrencies = bannedCurrencies; this.bannedPaymentMethods = bannedPaymentMethods; @@ -254,6 +269,8 @@ public Filter(List bannedOfferIds, this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys; this.disableAutoConf = disableAutoConf; this.bannedAutoConfExplorers = bannedAutoConfExplorers; + this.nodeAddressesBannedFromNetwork = nodeAddressesBannedFromNetwork; + this.disableApi = disableApi; // ownerPubKeyBytes can be null when called from tests if (ownerPubKeyBytes != null) { @@ -270,7 +287,7 @@ public protobuf.StoragePayload toProtoMessage() { .collect(Collectors.toList()); protobuf.Filter.Builder builder = protobuf.Filter.newBuilder().addAllBannedOfferIds(bannedOfferIds) - .addAllBannedNodeAddress(bannedNodeAddress) + .addAllNodeAddressesBannedFromTrading(nodeAddressesBannedFromTrading) .addAllBannedPaymentAccounts(paymentAccountFilterList) .addAllBannedCurrencies(bannedCurrencies) .addAllBannedPaymentMethods(bannedPaymentMethods) @@ -291,7 +308,9 @@ public protobuf.StoragePayload toProtoMessage() { .setCreationDate(creationDate) .addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys) .setDisableAutoConf(disableAutoConf) - .addAllBannedAutoConfExplorers(bannedAutoConfExplorers); + .addAllBannedAutoConfExplorers(bannedAutoConfExplorers) + .addAllNodeAddressesBannedFromNetwork(nodeAddressesBannedFromNetwork) + .setDisableApi(disableApi); Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); @@ -306,7 +325,7 @@ public static Filter fromProto(protobuf.Filter proto) { return new Filter(ProtoUtil.protocolStringListToList(proto.getBannedOfferIdsList()), - ProtoUtil.protocolStringListToList(proto.getBannedNodeAddressList()), + ProtoUtil.protocolStringListToList(proto.getNodeAddressesBannedFromTradingList()), bannedPaymentAccountsList, ProtoUtil.protocolStringListToList(proto.getBannedCurrenciesList()), ProtoUtil.protocolStringListToList(proto.getBannedPaymentMethodsList()), @@ -329,7 +348,9 @@ public static Filter fromProto(protobuf.Filter proto) { proto.getSignerPubKeyAsHex(), ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()), proto.getDisableAutoConf(), - ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()) + ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList()), + ProtoUtil.protocolStringListToSet(proto.getNodeAddressesBannedFromNetworkList()), + proto.getDisableApi() ); } @@ -347,7 +368,7 @@ public long getTTL() { public String toString() { return "Filter{" + "\n bannedOfferIds=" + bannedOfferIds + - ",\n bannedNodeAddress=" + bannedNodeAddress + + ",\n nodeAddressesBannedFromTrading=" + nodeAddressesBannedFromTrading + ",\n bannedAutoConfExplorers=" + bannedAutoConfExplorers + ",\n bannedPaymentAccounts=" + bannedPaymentAccounts + ",\n bannedCurrencies=" + bannedCurrencies + @@ -372,6 +393,8 @@ public String toString() { ",\n extraDataMap=" + extraDataMap + ",\n ownerPubKey=" + ownerPubKey + ",\n disableAutoConf=" + disableAutoConf + + ",\n nodeAddressesBannedFromNetwork=" + nodeAddressesBannedFromNetwork + + ",\n disableApi=" + disableApi + "\n}"; } } diff --git a/core/src/main/java/bisq/core/filter/FilterManager.java b/core/src/main/java/bisq/core/filter/FilterManager.java index 67f06220dd2..0dd35518c23 100644 --- a/core/src/main/java/bisq/core/filter/FilterManager.java +++ b/core/src/main/java/bisq/core/filter/FilterManager.java @@ -28,6 +28,7 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PServiceListener; +import bisq.network.p2p.network.NetworkFilter; import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.payload.ProtectedStorageEntry; @@ -115,6 +116,7 @@ public FilterManager(P2PService p2PService, Preferences preferences, Config config, ProvidersRepository providersRepository, + NetworkFilter networkFilter, @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.p2PService = p2PService; @@ -131,6 +133,7 @@ public FilterManager(P2PService p2PService, "029340c3e7d4bb0f9e651b5f590b434fecb6175aeaa57145c7804ff05d210e534f", "034dc7530bf66ffd9580aa98031ea9a18ac2d269f7c56c0e71eca06105b9ed69f9"); + networkFilter.setBannedNodeFunction(this::isNodeAddressBannedFromNetwork); } @@ -394,7 +397,13 @@ public boolean isOfferIdBanned(String offerId) { public boolean isNodeAddressBanned(NodeAddress nodeAddress) { return getFilter() != null && - getFilter().getBannedNodeAddress().stream() + getFilter().getNodeAddressesBannedFromTrading().stream() + .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); + } + + public boolean isNodeAddressBannedFromNetwork(NodeAddress nodeAddress) { + return getFilter() != null && + getFilter().getNodeAddressesBannedFromNetwork().stream() .anyMatch(e -> e.equals(nodeAddress.getFullAddress())); } diff --git a/core/src/main/java/bisq/core/network/CoreNetworkFilter.java b/core/src/main/java/bisq/core/network/CoreNetworkFilter.java new file mode 100644 index 00000000000..b261d421537 --- /dev/null +++ b/core/src/main/java/bisq/core/network/CoreNetworkFilter.java @@ -0,0 +1,58 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.network; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkFilter; + +import bisq.common.config.Config; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CoreNetworkFilter implements NetworkFilter { + private final Set bannedPeersFromOptions = new HashSet<>(); + private Function bannedNodeFunction; + + /** + * @param banList List of banned peers from program argument + */ + @Inject + public CoreNetworkFilter(@Named(Config.BAN_LIST) List banList) { + banList.stream().map(NodeAddress::new).forEach(bannedPeersFromOptions::add); + } + + @Override + public void setBannedNodeFunction(Function bannedNodeFunction) { + this.bannedNodeFunction = bannedNodeFunction; + } + + @Override + public boolean isPeerBanned(NodeAddress nodeAddress) { + return bannedPeersFromOptions.contains(nodeAddress) || + bannedNodeFunction != null && bannedNodeFunction.apply(nodeAddress); + } +} diff --git a/core/src/main/java/bisq/core/offer/AvailabilityResult.java b/core/src/main/java/bisq/core/offer/AvailabilityResult.java index 2d3d749ff24..18e877c8306 100644 --- a/core/src/main/java/bisq/core/offer/AvailabilityResult.java +++ b/core/src/main/java/bisq/core/offer/AvailabilityResult.java @@ -27,5 +27,7 @@ public enum AvailabilityResult { NO_MEDIATORS, USER_IGNORED, MISSING_MANDATORY_CAPABILITY, - NO_REFUND_AGENTS + NO_REFUND_AGENTS, + UNCONF_TX_LIMIT_HIT, + MAKER_DENIED_API_USER } diff --git a/core/src/main/java/bisq/core/offer/OfferFilter.java b/core/src/main/java/bisq/core/offer/OfferFilter.java new file mode 100644 index 00000000000..c22231de5bb --- /dev/null +++ b/core/src/main/java/bisq/core/offer/OfferFilter.java @@ -0,0 +1,209 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.offer; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.common.app.Version; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.SetChangeListener; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class OfferFilter { + private final User user; + private final Preferences preferences; + private final FilterManager filterManager; + private final AccountAgeWitnessService accountAgeWitnessService; + private final Map insufficientCounterpartyTradeLimitCache = new HashMap<>(); + private final Map myInsufficientTradeLimitCache = new HashMap<>(); + + @Inject + public OfferFilter(User user, + Preferences preferences, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService) { + this.user = user; + this.preferences = preferences; + this.filterManager = filterManager; + this.accountAgeWitnessService = accountAgeWitnessService; + + if (user != null) { + // If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) c -> + myInsufficientTradeLimitCache.clear()); + } + } + + public enum Result { + VALID(true), + API_DISABLED, + HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER, + HAS_NOT_SAME_PROTOCOL_VERSION, + IS_IGNORED, + IS_OFFER_BANNED, + IS_CURRENCY_BANNED, + IS_PAYMENT_METHOD_BANNED, + IS_NODE_ADDRESS_BANNED, + REQUIRE_UPDATE_TO_NEW_VERSION, + IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT, + IS_MY_INSUFFICIENT_TRADE_LIMIT; + + @Getter + private final boolean isValid; + + Result(boolean isValid) { + this.isValid = isValid; + } + + Result() { + this(false); + } + } + + public Result canTakeOffer(Offer offer, boolean isTakerApiUser) { + if (isTakerApiUser && filterManager.getFilter() != null && filterManager.getFilter().isDisableApi()) { + return Result.API_DISABLED; + } + if (!isAnyPaymentAccountValidForOffer(offer)) { + return Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER; + } + if (!hasSameProtocolVersion(offer)) { + return Result.HAS_NOT_SAME_PROTOCOL_VERSION; + } + if (isIgnored(offer)) { + return Result.IS_IGNORED; + } + if (isOfferBanned(offer)) { + return Result.IS_OFFER_BANNED; + } + if (isCurrencyBanned(offer)) { + return Result.IS_CURRENCY_BANNED; + } + if (isPaymentMethodBanned(offer)) { + return Result.IS_PAYMENT_METHOD_BANNED; + } + if (isNodeAddressBanned(offer)) { + return Result.IS_NODE_ADDRESS_BANNED; + } + if (requireUpdateToNewVersion()) { + return Result.REQUIRE_UPDATE_TO_NEW_VERSION; + } + if (isInsufficientCounterpartyTradeLimit(offer)) { + return Result.IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT; + } + if (isMyInsufficientTradeLimit(offer)) { + return Result.IS_MY_INSUFFICIENT_TRADE_LIMIT; + } + + return Result.VALID; + } + + public boolean isAnyPaymentAccountValidForOffer(Offer offer) { + return user.getPaymentAccounts() != null && + PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); + } + + public boolean hasSameProtocolVersion(Offer offer) { + return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; + } + + public boolean isIgnored(Offer offer) { + return preferences.getIgnoreTradersList().stream() + .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); + } + + public boolean isOfferBanned(Offer offer) { + return filterManager.isOfferIdBanned(offer.getId()); + } + + public boolean isCurrencyBanned(Offer offer) { + return filterManager.isCurrencyBanned(offer.getCurrencyCode()); + } + + public boolean isPaymentMethodBanned(Offer offer) { + return filterManager.isPaymentMethodBanned(offer.getPaymentMethod()); + } + + public boolean isNodeAddressBanned(Offer offer) { + return filterManager.isNodeAddressBanned(offer.getMakerNodeAddress()); + } + + public boolean requireUpdateToNewVersion() { + return filterManager.requireUpdateToNewVersionForTrading(); + } + + // This call is a bit expensive so we cache results + public boolean isInsufficientCounterpartyTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (insufficientCounterpartyTradeLimitCache.containsKey(offerId)) { + return insufficientCounterpartyTradeLimitCache.get(offerId); + } + + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), + errorMessage -> { + }); + insufficientCounterpartyTradeLimitCache.put(offerId, result); + return result; + } + + // This call is a bit expensive so we cache results + public boolean isMyInsufficientTradeLimit(Offer offer) { + String offerId = offer.getId(); + if (myInsufficientTradeLimitCache.containsKey(offerId)) { + return myInsufficientTradeLimitCache.get(offerId); + } + + Optional accountOptional = PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, + user.getPaymentAccounts(), + accountAgeWitnessService); + long myTradeLimit = accountOptional + .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection())) + .orElse(0L); + long offerMinAmount = offer.getMinAmount().value; + log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", + accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", + Coin.valueOf(myTradeLimit).toFriendlyString(), + Coin.valueOf(offerMinAmount).toFriendlyString()); + boolean result = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + accountOptional.isPresent() && + myTradeLimit < offerMinAmount; + myInsufficientTradeLimitCache.put(offerId, result); + return result; + } +} diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 3e16c784024..3f07eaa1f45 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -634,47 +634,50 @@ private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, No 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; - - mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); - openOffer.setMediatorNodeAddress(mediatorNodeAddress); - - 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; + 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); + + 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()); + 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 void onFault(String errorMessage) { } } + private boolean apiUserDeniedByOffer(OfferAvailabilityRequest request) { + return preferences.isDenyApiTaker() && request.isTakerApiUser(); + } + private void sendAckMessage(OfferAvailabilityRequest message, NodeAddress sender, boolean result, diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java index 8d183d40857..c1559cec8d3 100644 --- a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java @@ -66,19 +66,24 @@ public class OfferAvailabilityModel implements Model { @Getter private NodeAddress selectedRefundAgent; + // Added in v1.5.5 + @Getter + private final boolean isTakerApiUser; public OfferAvailabilityModel(Offer offer, PubKeyRing pubKeyRing, P2PService p2PService, User user, MediatorManager mediatorManager, - TradeStatisticsManager tradeStatisticsManager) { + TradeStatisticsManager tradeStatisticsManager, + boolean isTakerApiUser) { this.offer = offer; this.pubKeyRing = pubKeyRing; this.p2PService = p2PService; this.user = user; this.mediatorManager = mediatorManager; this.tradeStatisticsManager = tradeStatisticsManager; + this.isTakerApiUser = isTakerApiUser; } public NodeAddress getPeerNodeAddress() { diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java index 3ee88536c64..0dbc8e69ea4 100644 --- a/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java +++ b/core/src/main/java/bisq/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -39,7 +39,8 @@ protected void run() { try { runInterceptHook(); - OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(), model.getPubKeyRing(), model.getTakersTradePrice()); + OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(), + model.getPubKeyRing(), model.getTakersTradePrice(), model.isTakerApiUser()); log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getOfferId(), message.getUid(), model.getPeerNodeAddress()); diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java index a3cbd1c9d06..6d9d14eaf61 100644 --- a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java @@ -42,13 +42,16 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp private final long takersTradePrice; @Nullable private final Capabilities supportedCapabilities; + private final boolean isTakerApiUser; public OfferAvailabilityRequest(String offerId, PubKeyRing pubKeyRing, - long takersTradePrice) { + long takersTradePrice, + boolean isTakerApiUser) { this(offerId, pubKeyRing, takersTradePrice, + isTakerApiUser, Capabilities.app, Version.getP2PMessageVersion(), UUID.randomUUID().toString()); @@ -62,12 +65,14 @@ public OfferAvailabilityRequest(String offerId, 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 protobuf.NetworkEnvelope toProtoNetworkEnvelope() { 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 static OfferAvailabilityRequest fromProto(protobuf.OfferAvailabilityReque return new OfferAvailabilityRequest(proto.getOfferId(), PubKeyRing.fromProto(proto.getPubKeyRing()), proto.getTakersTradePrice(), + proto.getIsTakerApiUser(), Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion, proto.getUid().isEmpty() ? null : proto.getUid()); diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index dc87d4a98cc..6707fde8ea7 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -373,6 +373,7 @@ public void requestPersistence() { /////////////////////////////////////////////////////////////////////////////////////////// public void checkOfferAvailability(Offer offer, + boolean isTakerApiUser, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (btcWalletService.isUnconfirmedTransactionsLimitHit() || @@ -383,7 +384,7 @@ public void checkOfferAvailability(Offer offer, 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 void onTakeOffer(Coin amount, 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 @@ private ProcessModel getNewProcessModel(Offer offer) { processModelServiceProvider.getKeyRing().getPubKeyRing()); } - private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer) { + private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer, boolean isTakerApiUser) { return new OfferAvailabilityModel( offer, keyRing.getPubKeyRing(), p2PService, user, mediatorManager, - tradeStatisticsManager); + tradeStatisticsManager, + isTakerApiUser); } diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 4dd43b85d2a..ee276d0ef03 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -487,6 +487,11 @@ public void setAutoConfTradeLimit(String currencyCode, long tradeLimit) { }); } + public void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods) { + prefPayload.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods); + requestPersistence(); + } + private void requestPersistence() { if (initialReadDone) persistenceManager.requestPersistence(); @@ -767,6 +772,16 @@ public void setIgnoreDustThreshold(int value) { requestPersistence(); } + public void setShowOffersMatchingMyAccounts(boolean value) { + prefPayload.setShowOffersMatchingMyAccounts(value); + requestPersistence(); + } + + public void setDenyApiTaker(boolean value) { + prefPayload.setDenyApiTaker(value); + requestPersistence(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getter @@ -1074,5 +1089,11 @@ private interface ExcludesDelegateMethods { void setBsqAverageTrimThreshold(double bsqAverageTrimThreshold); void setAutoConfirmSettings(AutoConfirmSettings autoConfirmSettings); + + void setHideNonAccountPaymentMethods(boolean hideNonAccountPaymentMethods); + + void setShowOffersMatchingMyAccounts(boolean value); + + void setDenyApiTaker(boolean value); } } diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index 0a7aede7e36..b8991f8cbae 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -129,6 +129,10 @@ public final class PreferencesPayload implements PersistableEnvelope { // Added at 1.3.8 private List autoConfirmSettingsList = new ArrayList<>(); + // Added in 1.5.5 + private boolean hideNonAccountPaymentMethods; + private boolean showOffersMatchingMyAccounts; + private boolean denyApiTaker; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -192,7 +196,10 @@ public Message toProtoMessage() { .setBsqAverageTrimThreshold(bsqAverageTrimThreshold) .addAllAutoConfirmSettings(autoConfirmSettingsList.stream() .map(autoConfirmSettings -> ((protobuf.AutoConfirmSettings) autoConfirmSettings.toProtoMessage())) - .collect(Collectors.toList())); + .collect(Collectors.toList())) + .setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods) + .setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts) + .setDenyApiTaker(denyApiTaker); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); @@ -286,7 +293,10 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co proto.getAutoConfirmSettingsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAutoConfirmSettingsList().stream() .map(AutoConfirmSettings::fromProto) - .collect(Collectors.toList())) + .collect(Collectors.toList())), + proto.getHideNonAccountPaymentMethods(), + proto.getShowOffersMatchingMyAccounts(), + proto.getDenyApiTaker() ); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4a3f82283c4..2f8366f4268 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -340,6 +340,7 @@ offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n { offerbook.availableOffers=Available offers offerbook.filterByCurrency=Filter by currency offerbook.filterByPaymentMethod=Filter by payment method +offerbook.matchingOffers=Offers matching my accounts offerbook.timeSinceSigning=Account info offerbook.timeSinceSigning.info=This account was verified and {0} offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts @@ -1219,6 +1220,8 @@ setting.preferences.showOwnOffers=Show my own offers in offer book 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}. @@ -2594,7 +2597,8 @@ enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=Edit filter list filterWindow.offers=Filtered offers (comma sep.) -filterWindow.onions=Filtered onion addresses (comma sep.) +filterWindow.onions=Banned from trading addresses (comma sep.) +filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value] filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) @@ -2615,6 +2619,7 @@ filterWindow.disableTradeBelowVersion=Min. version required for trading filterWindow.add=Add filter filterWindow.remove=Remove filter filterWindow.btcFeeReceiverAddresses=BTC fee receiver addresses +filterWindow.disableApi=Disable API offerDetailsWindow.minBtcAmount=Min. BTC amount offerDetailsWindow.min=(min. {0}) diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 144e299181c..4b7f6e3f79e 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -25,6 +25,8 @@ import com.google.common.collect.Lists; +import java.util.HashSet; + import org.junit.Ignore; public class UserPayloadModelVOTest { @@ -64,7 +66,8 @@ public void testRoundtripFull() { null, null, false, - Lists.newArrayList())); + Lists.newArrayList(), + new HashSet<>())); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); diff --git a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java index ca7b77bcffb..d605e01efc5 100644 --- a/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java +++ b/core/src/test/java/bisq/core/util/FeeReceiverSelectorTest.java @@ -26,6 +26,7 @@ import com.google.common.primitives.Longs; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; @@ -123,6 +124,7 @@ private static Filter filterWithReceivers(List btcFeeReceiverAddresses) null, null, false, - Lists.newArrayList()); + Lists.newArrayList(), + new HashSet<>()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java index bdbb89cb331..8d435ede855 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -236,7 +236,7 @@ public void activate() { if (DevEnv.isDevMode()) { UserThread.runAfter(() -> { amount.set("0.001"); - price.set("70000"); + price.set("210000"); minAmount.set(amount.get()); onFocusOutPriceAsPercentageTextField(true, false); applyMakerFee(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index e1133a16b24..caa01deea4a 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -22,6 +22,7 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipSlideToggleButton; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.ColoredDecimalPlacesWithZerosText; @@ -51,6 +52,7 @@ 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; @@ -61,6 +63,7 @@ import bisq.network.p2p.NodeAddress; +import bisq.common.app.DevEnv; import bisq.common.config.Config; import bisq.common.util.Tuple3; @@ -126,6 +129,7 @@ public class OfferBookView extends ActivatableViewAndModel currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; + private AutoTooltipSlideToggleButton matchingOffersToggle; private AutoTooltipTableColumn amountColumn, volumeColumn, marketColumn, priceColumn, paymentMethodColumn, depositColumn, signingStateColumn, avatarColumn; private TableView tableView; @@ -174,10 +178,25 @@ public void initialize() { hBox.setSpacing(35); hBox.setPadding(new Insets(10, 0, 0, 0)); - final Tuple3> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( + Tuple3> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByCurrency")); - final Tuple3> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( + currencyComboBox = currencyBoxTuple.third; + currencyComboBox.setPrefWidth(270); + + Tuple3> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); + paymentMethodComboBox = paymentBoxTuple.third; + paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); + paymentMethodComboBox.setPrefWidth(270); + + matchingOffersToggle = new AutoTooltipSlideToggleButton(); + matchingOffersToggle.setText(Res.get("offerbook.matchingOffers")); + HBox.setMargin(matchingOffersToggle, new Insets(7, 0, -9, -15)); + + hBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, matchingOffersToggle); + AnchorPane.setLeftAnchor(hBox, 0d); + AnchorPane.setTopAnchor(hBox, 0d); + AnchorPane.setBottomAnchor(hBox, 0d); createOfferButton = new AutoTooltipButton(); createOfferButton.setMinHeight(40); @@ -185,11 +204,6 @@ public void initialize() { AnchorPane.setRightAnchor(createOfferButton, 0d); AnchorPane.setBottomAnchor(createOfferButton, 0d); - hBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, createOfferButton); - AnchorPane.setLeftAnchor(hBox, 0d); - AnchorPane.setTopAnchor(hBox, 0d); - AnchorPane.setBottomAnchor(hBox, 0d); - AnchorPane anchorPane = new AnchorPane(); anchorPane.getChildren().addAll(hBox, createOfferButton); @@ -199,11 +213,6 @@ public void initialize() { GridPane.setMargin(anchorPane, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); root.getChildren().add(anchorPane); - currencyComboBox = currencyBoxTuple.third; - - paymentMethodComboBox = paymentBoxTuple.third; - paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); - tableView = new TableView<>(); GridPane.setRowIndex(tableView, ++gridRow); @@ -324,13 +333,14 @@ protected void activate() { currencyComboBox.getSelectionModel().select(SHOW_ALL); model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); }); + updateCurrencyComboBoxFromModel(); - if (model.showAllTradeCurrenciesProperty.get()) - currencyComboBox.getSelectionModel().select(SHOW_ALL); - else - currencyComboBox.getSelectionModel().select(model.getSelectedTradeCurrency()); currencyComboBox.getEditor().setText(new CurrencyStringConverter(currencyComboBox).toString(currencyComboBox.getSelectionModel().getSelectedItem())); + matchingOffersToggle.setSelected(model.useOffersMatchingMyAccountsFilter); + matchingOffersToggle.disableProperty().bind(model.disableMatchToggle); + matchingOffersToggle.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggle.isSelected())); + volumeColumn.sortableProperty().bind(model.showAllTradeCurrenciesProperty.not()); model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty()); @@ -359,6 +369,7 @@ protected void activate() { if (paymentMethodComboBox.getEditor().getText().isEmpty()) paymentMethodComboBox.getSelectionModel().select(SHOW_ALL); model.onSetPaymentMethod(paymentMethodComboBox.getSelectionModel().getSelectedItem()); + updateCurrencyComboBoxFromModel(); updateSigningStateColumn(); }); @@ -405,6 +416,14 @@ protected void activate() { model.priceFeedService.updateCounterProperty().addListener(priceFeedUpdateCounterListener); } + private void updateCurrencyComboBoxFromModel() { + if (model.showAllTradeCurrenciesProperty.get()) { + currencyComboBox.getSelectionModel().select(SHOW_ALL); + } else { + currencyComboBox.getSelectionModel().select(model.getSelectedTradeCurrency()); + } + } + private void updateSigningStateColumn() { if (model.hasSelectionAccountSigning()) { if (!tableView.getColumns().contains(signingStateColumn)) { @@ -418,6 +437,8 @@ private void updateSigningStateColumn() { @Override protected void deactivate() { createOfferButton.setOnAction(null); + matchingOffersToggle.setOnAction(null); + matchingOffersToggle.disableProperty().unbind(); model.getOfferList().comparatorProperty().unbind(); volumeColumn.sortableProperty().unbind(); @@ -598,51 +619,61 @@ private void onCreateOffer() { } } - 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 account = model.getMostMaturePaymentAccountForOffer(offer); - if (account.isPresent()) { - final long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), - offer.getCurrencyCode(), offer.getMirroredDirection()); - new Popup() - .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", - formatter.formatCoinWithCode(Coin.valueOf(tradeLimit)), - Res.get("offerbook.warning.newVersionAnnouncement"))) - .show(); - } else { - log.warn("We don't found a payment account but got called the isInsufficientTradeLimit case. That must not happen."); - } + private void onShowInfo(Offer offer, OfferFilter.Result result) { + switch (result) { + case VALID: + break; + case API_DISABLED: + DevEnv.logErrorAndThrowIfDevMode("We are in desktop and in the taker position " + + "viewing offers, so it cannot be that we got that result as we are not an API user."); + break; + case HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER: + openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"), + Res.get("offerbook.warning.noMatchingAccount.msg"), + FiatAccountsView.class, + "navigation.account"); + break; + case HAS_NOT_SAME_PROTOCOL_VERSION: + new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show(); + break; + case IS_IGNORED: + new Popup().warning(Res.get("offerbook.warning.userIgnored")).show(); + break; + case IS_OFFER_BANNED: + new Popup().warning(Res.get("offerbook.warning.offerBlocked")).show(); + break; + case IS_CURRENCY_BANNED: + new Popup().warning(Res.get("offerbook.warning.currencyBanned")).show(); + break; + case IS_PAYMENT_METHOD_BANNED: + new Popup().warning(Res.get("offerbook.warning.paymentMethodBanned")).show(); + break; + case IS_NODE_ADDRESS_BANNED: + new Popup().warning(Res.get("offerbook.warning.nodeBlocked")).show(); + break; + case REQUIRE_UPDATE_TO_NEW_VERSION: + new Popup().warning(Res.get("offerbook.warning.requireUpdateToNewVersion")).show(); + break; + case IS_INSUFFICIENT_COUNTERPARTY_TRADE_LIMIT: + new Popup().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show(); + break; + case IS_MY_INSUFFICIENT_TRADE_LIMIT: + Optional account = model.getMostMaturePaymentAccountForOffer(offer); + if (account.isPresent()) { + long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), + offer.getCurrencyCode(), offer.getMirroredDirection()); + new Popup() + .warning(Res.get("popup.warning.tradeLimitDueAccountAgeRestriction.buyer", + formatter.formatCoinWithCode(Coin.valueOf(tradeLimit)), + Res.get("offerbook.warning.newVersionAnnouncement"))) + .show(); + } else { + DevEnv.logErrorAndThrowIfDevMode("We don't found a payment account but got called the " + + "isInsufficientTradeLimit case."); + } + break; + default: + break; } } @@ -996,11 +1027,7 @@ public TableCell call(TableColumn() { 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); @@ -1015,33 +1042,14 @@ public void updateItem(final OfferBookListItem item, boolean empty) { TableRow tableRow = getTableRow(); if (item != null && !empty) { - final Offer offer = item.getOffer(); + Offer offer = item.getOffer(); boolean myOffer = model.isMyOffer(offer); + if (tableRow != null) { - 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 (isTradable) { + canTakeOfferResult = model.offerFilter.canTakeOffer(offer, false); + tableRow.setOpacity(canTakeOfferResult.isValid() || myOffer ? 1 : 0.4); + + if (canTakeOfferResult.isValid()) { // set first row button as default button.setDefaultButton(getIndex() == 0); tableRow.setOnMousePressed(null); @@ -1050,17 +1058,7 @@ public void updateItem(final OfferBookListItem item, boolean empty) { 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); }); } } @@ -1090,18 +1088,15 @@ public void updateItem(final OfferBookListItem item, boolean empty) { 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)); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index 98737f4036c..6e0ddd0fa3d 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -28,7 +28,6 @@ import bisq.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.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.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; -import bisq.common.app.Version; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; @@ -87,6 +86,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -101,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; @@ -121,15 +122,17 @@ class OfferBookViewModel extends ActivatableViewModel { // If id is empty string we ignore filter (display all methods) - PaymentMethod selectedPaymentMethod = PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); + PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); private boolean isTabSelected; final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); + final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForVolume = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForPrice = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; + boolean useOffersMatchingMyAccountsFilter; /////////////////////////////////////////////////////////////////////////////////////////// @@ -145,10 +148,10 @@ public OfferBookViewModel(User user, 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(); @@ -161,10 +164,10 @@ public OfferBookViewModel(User user, 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; @@ -212,7 +215,7 @@ protected void activate() { filteredItems.addListener(filterItemsListener); String code = direction == OfferPayload.Direction.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode(); - if (code != null && !code.equals(GUIUtil.SHOW_ALL_FLAG) && !code.isEmpty() && + if (code != null && !code.isEmpty() && !isShowAllEntry(code) && CurrencyUtil.getTradeCurrency(code).isPresent()) { showAllTradeCurrenciesProperty.set(false); selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get(); @@ -222,10 +225,15 @@ protected void activate() { } tradeCurrencyCode.set(selectedTradeCurrency.getCode()); + if (user != null) { + disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty()); + } + useOffersMatchingMyAccountsFilter = !disableMatchToggle.get() && isShowOffersMatchingMyAccounts(); + fillAllTradeCurrencies(); preferences.getTradeCurrenciesAsObservable().addListener(tradeCurrencyListChangeListener); offerBook.fillOfferBookListItems(); - applyFilterPredicate(); + filterOffers(); setMarketPriceFeedCurrency(); priceUtil.recalculateBsq30DayAveragePrice(); @@ -237,6 +245,7 @@ protected void deactivate() { preferences.getTradeCurrenciesAsObservable().removeListener(tradeCurrencyListChangeListener); } + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -267,7 +276,7 @@ else if (!showAllEntry) { } setMarketPriceFeedCurrency(); - applyFilterPredicate(); + filterOffers(); if (direction == OfferPayload.Direction.BUY) preferences.setBuyScreenCurrencyCode(code); @@ -281,22 +290,40 @@ void onSetPaymentMethod(PaymentMethod paymentMethod) { return; showAllPaymentMethods = isShowAllEntry(paymentMethod.getId()); - if (!showAllPaymentMethods) + if (!showAllPaymentMethods) { this.selectedPaymentMethod = paymentMethod; - else - this.selectedPaymentMethod = PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); - applyFilterPredicate(); + // If we select TransferWise we switch to show all currencies as TransferWise supports + // sending to most currencies. + if (paymentMethod.getId().equals(PaymentMethod.TRANSFERWISE_ID)) { + onSetTradeCurrency(getShowAllEntryForCurrency()); + } + } else { + this.selectedPaymentMethod = getShowAllEntryForPaymentMethod(); + } + + filterOffers(); } void onRemoveOpenOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { openOfferManager.removeOffer(offer, resultHandler, errorMessageHandler); } + void onShowOffersMatchingMyAccounts(boolean isSelected) { + useOffersMatchingMyAccountsFilter = isSelected; + preferences.setShowOffersMatchingMyAccounts(useOffersMatchingMyAccountsFilter); + filterOffers(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// + boolean isShowOffersMatchingMyAccounts() { + return preferences.isShowOffersMatchingMyAccounts(); + } + SortedList getOfferList() { return sortedItems; } @@ -331,8 +358,16 @@ TradeCurrency getSelectedTradeCurrency() { ObservableList getPaymentMethods() { ObservableList list = FXCollections.observableArrayList(PaymentMethod.getPaymentMethods()); + if (preferences.isHideNonAccountPaymentMethods() && user.getPaymentAccounts() != null) { + Set supportedPaymentMethods = user.getPaymentAccounts().stream() + .map(PaymentAccount::getPaymentMethod).collect(Collectors.toSet()); + if (!supportedPaymentMethods.isEmpty()) { + list = FXCollections.observableArrayList(supportedPaymentMethods); + } + } + list.sort(Comparator.naturalOrder()); - list.add(0, PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG)); + list.add(0, getShowAllEntryForPaymentMethod()); return list; } @@ -512,20 +547,16 @@ private void setMarketPriceFeedCurrency() { private void fillAllTradeCurrencies() { allTradeCurrencies.clear(); // Used for ignoring filter (show all) - allTradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, "")); + allTradeCurrencies.add(getShowAllEntryForCurrency()); allTradeCurrencies.addAll(preferences.getTradeCurrenciesAsObservable()); - allTradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, "")); + allTradeCurrencies.add(getEditEntryForCurrency()); } + /////////////////////////////////////////////////////////////////////////////////////////// // Checks /////////////////////////////////////////////////////////////////////////////////////////// - boolean isAnyPaymentAccountValidForOffer(Offer offer) { - return user.getPaymentAccounts() != null && - PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); - } - boolean hasPaymentAccountForCurrency() { return (showAllTradeCurrenciesProperty.get() && user.getPaymentAccounts() != null && @@ -544,8 +575,15 @@ boolean canCreateOrTakeOffer() { // Filters /////////////////////////////////////////////////////////////////////////////////////////// - private void applyFilterPredicate() { - filteredItems.setPredicate(offerBookListItem -> { + private void filterOffers() { + Predicate predicate = useOffersMatchingMyAccountsFilter ? + getCurrencyAndMethodPredicate().and(getOffersMatchingMyAccountsPredicate()) : + getCurrencyAndMethodPredicate(); + filteredItems.setPredicate(predicate); + } + + private Predicate getCurrencyAndMethodPredicate() { + return offerBookListItem -> { Offer offer = offerBookListItem.getOffer(); boolean directionResult = offer.getDirection() != direction; boolean currencyResult = (showAllTradeCurrenciesProperty.get()) || @@ -554,58 +592,18 @@ private void applyFilterPredicate() { offer.getPaymentMethod().equals(selectedPaymentMethod); boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook(); return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated; - }); + }; } - boolean isIgnored(Offer offer) { - return preferences.getIgnoreTradersList().stream() - .anyMatch(i -> i.equals(offer.getMakerNodeAddress().getFullAddress())); + private Predicate getOffersMatchingMyAccountsPredicate() { + // 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 -> 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(); - } - - boolean isInsufficientCounterpartyTradeLimit(Offer offer) { - return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), errorMessage -> { - }); - } - - boolean isMyInsufficientTradeLimit(Offer offer) { - Optional accountOptional = getMostMaturePaymentAccountForOffer(offer); - long myTradeLimit = accountOptional - .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, - offer.getCurrencyCode(), offer.getMirroredDirection())) - .orElse(0L); - long offerMinAmount = offer.getMinAmount().value; - log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", - accountOptional.isPresent() ? accountOptional.get().getAccountName() : "null", - Coin.valueOf(myTradeLimit).toFriendlyString(), - Coin.valueOf(offerMinAmount).toFriendlyString()); - return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && - accountOptional.isPresent() && - myTradeLimit < offerMinAmount; - } - - boolean hasSameProtocolVersion(Offer offer) { - return offer.getProtocolVersion() == Version.TRADE_PROTOCOL_VERSION; + return offerFilter.isOfferBanned(offer); } private boolean isShowAllEntry(String id) { @@ -629,11 +627,11 @@ int getNumTrades(Offer offer) { public boolean hasSelectionAccountSigning() { if (showAllTradeCurrenciesProperty.get()) { - if (!selectedPaymentMethod.getId().equals(GUIUtil.SHOW_ALL_FLAG)) { + if (!isShowAllEntry(selectedPaymentMethod.getId())) { return PaymentMethod.hasChargebackRisk(selectedPaymentMethod); } } else { - if (selectedPaymentMethod.getId().equals(GUIUtil.SHOW_ALL_FLAG)) + if (isShowAllEntry(selectedPaymentMethod.getId())) return CurrencyUtil.getMatureMarketCurrencies().stream() .anyMatch(c -> c.getCode().equals(selectedTradeCurrency.getCode())); else @@ -659,4 +657,16 @@ public String formatDepositString(Coin deposit, long amount) { var percentage = FormattingUtils.formatToRoundedPercentWithSymbol(deposit.getValue() / (double) amount); return btcFormatter.formatCoin(deposit) + " (" + percentage + ")"; } + + private TradeCurrency getShowAllEntryForCurrency() { + return new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""); + } + + private TradeCurrency getEditEntryForCurrency() { + return new CryptoCurrency(GUIUtil.EDIT_FLAG, ""); + } + + private PaymentMethod getShowAllEntryForPaymentMethod() { + return PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index dc90ce04559..9c3eacd3308 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -171,6 +171,7 @@ protected void activate() { if (canTakeOffer()) { tradeManager.checkOfferAvailability(offer, + false, () -> { }, errorMessage -> new Popup().warning(errorMessage).show()); @@ -319,7 +320,8 @@ void onTakeOffer(TradeResultHandler tradeResultHandler) { offer, paymentAccount.getId(), useSavingsWallet, - tradeResultHandler::handleResult, + false, + tradeResultHandler, errorMessage -> { log.warn(errorMessage); new Popup().warning(errorMessage).show(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index a82f00d02b7..5ff3f216eab 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -37,8 +37,6 @@ import org.apache.commons.lang3.StringUtils; -import javafx.collections.FXCollections; - import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; @@ -51,7 +49,11 @@ import javafx.geometry.HPos; import javafx.geometry.Insets; +import javafx.collections.FXCollections; + import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; @@ -122,9 +124,11 @@ private void addContent() { InputTextField offerIdsTF = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.offers")); - InputTextField nodesTF = addTopLabelInputTextField(gridPane, ++rowIndex, + InputTextField bannedFromTradingTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.onions")).second; - nodesTF.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate + InputTextField bannedFromNetworkTF = addTopLabelInputTextField(gridPane, ++rowIndex, + Res.get("filterWindow.bannedFromNetwork")).second; + bannedFromTradingTF.setPromptText("E.g. zqnzx6o3nifef5df.onion:9999"); // Do not translate InputTextField paymentAccountFilterTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.accounts")).second; GridPane.setHalignment(paymentAccountFilterTF, HPos.RIGHT); @@ -165,11 +169,14 @@ private void addContent() { Res.get("filterWindow.bannedPrivilegedDevPubKeys")).second; InputTextField autoConfExplorersTF = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.autoConfExplorers")).second; + CheckBox disableApiCheckBox = addLabelCheckBox(gridPane, ++rowIndex, + Res.get("filterWindow.disableApi")); Filter filter = filterManager.getDevFilter(); if (filter != null) { setupFieldFromList(offerIdsTF, filter.getBannedOfferIds()); - setupFieldFromList(nodesTF, filter.getBannedNodeAddress()); + setupFieldFromList(bannedFromTradingTF, filter.getNodeAddressesBannedFromTrading()); + setupFieldFromList(bannedFromNetworkTF, filter.getNodeAddressesBannedFromNetwork()); setupFieldFromPaymentAccountFiltersList(paymentAccountFilterTF, filter.getBannedPaymentAccounts()); setupFieldFromList(bannedCurrenciesTF, filter.getBannedCurrencies()); setupFieldFromList(bannedPaymentMethodsTF, filter.getBannedPaymentMethods()); @@ -189,6 +196,7 @@ private void addContent() { disableAutoConfCheckBox.setSelected(filter.isDisableAutoConf()); disableDaoBelowVersionTF.setText(filter.getDisableDaoBelowVersion()); disableTradeBelowVersionTF.setText(filter.getDisableTradeBelowVersion()); + disableApiCheckBox.setSelected(filter.isDisableApi()); } Button removeFilterMessageButton = new AutoTooltipButton(Res.get("filterWindow.remove")); @@ -201,7 +209,7 @@ private void addContent() { String signerPubKeyAsHex = filterManager.getSignerPubKeyAsHex(privKeyString); Filter newFilter = new Filter( readAsList(offerIdsTF), - readAsList(nodesTF), + readAsList(bannedFromTradingTF), readAsPaymentAccountFiltersList(paymentAccountFilterTF), readAsList(bannedCurrenciesTF), readAsList(bannedPaymentMethodsTF), @@ -221,7 +229,9 @@ private void addContent() { signerPubKeyAsHex, readAsList(bannedPrivilegedDevPubKeysTF), disableAutoConfCheckBox.isSelected(), - readAsList(autoConfExplorersTF) + readAsList(autoConfExplorersTF), + new HashSet<>(readAsList(bannedFromNetworkTF)), + disableApiCheckBox.isSelected() ); // We remove first the old filter @@ -270,7 +280,7 @@ private void addDevFilter(Button removeFilterMessageButton, String privKeyString hide(); } - private void setupFieldFromList(InputTextField field, List values) { + private void setupFieldFromList(InputTextField field, Collection values) { if (values != null) field.setText(String.join(", ", values)); } diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 8d000b3139b..19f4b736071 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -45,8 +45,11 @@ import bisq.core.locale.LanguageUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; +import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; import bisq.core.util.coin.CoinFormatter; @@ -100,13 +103,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static bisq.desktop.util.FormBuilder.*; import static com.google.common.base.Preconditions.checkArgument; @FxmlView public class PreferencesView extends ActivatableViewAndModel { + private final User user; private final CoinFormatter formatter; private TextField btcExplorerTextField, bsqExplorerTextField; private ComboBox userLanguageComboBox; @@ -114,7 +120,7 @@ public class PreferencesView extends ActivatableViewAndModel preferredTradeCurrencyComboBox; private ToggleButton showOwnOffersInOfferBook, useAnimations, useDarkMode, sortMarketCurrenciesNumerically, - avoidStandbyMode, useCustomFee, autoConfirmXmrToggle; + avoidStandbyMode, useCustomFee, autoConfirmXmrToggle, hideNonAccountPaymentMethodsToggle, denyApiTakerToggle; private int gridRow = 0; private int displayCurrenciesGridRowIndex = 0; private InputTextField transactionFeeInputTextField, ignoreTradersListInputTextField, ignoreDustThresholdInputTextField, @@ -171,12 +177,14 @@ public PreferencesView(PreferencesViewModel model, FilterManager filterManager, DaoFacade daoFacade, Config config, + User user, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @Named(Config.RPC_USER) String rpcUser, @Named(Config.RPC_PASSWORD) String rpcPassword, @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockNotificationPort, @Named(Config.STORAGE_DIR) File storageDir) { super(model); + this.user = user; this.formatter = formatter; this.preferences = preferences; this.feeService = feeService; @@ -595,14 +603,15 @@ public CryptoCurrency fromString(String s) { } private void initializeDisplayOptions() { - TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 5, Res.get("setting.preferences.displayOptions"), Layout.GROUP_DISTANCE); + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 7, Res.get("setting.preferences.displayOptions"), Layout.GROUP_DISTANCE); GridPane.setColumnSpan(titledGroupBg, 1); showOwnOffersInOfferBook = addSlideToggleButton(root, gridRow, Res.get("setting.preferences.showOwnOffers"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); useAnimations = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useAnimations")); useDarkMode = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useDarkMode")); - // useStickyMarketPriceCheckBox = addLabelCheckBox(root, ++gridRow, "Use sticky market price:", "").second; sortMarketCurrenciesNumerically = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.sortWithNumOffers")); + hideNonAccountPaymentMethodsToggle = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.onlyShowPaymentMethodsFromAccount")); + denyApiTakerToggle = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.denyApiTaker")); resetDontShowAgainButton = addButton(root, ++gridRow, Res.get("setting.preferences.resetAllFlags"), 0); resetDontShowAgainButton.getStyleClass().add("compact-button"); resetDontShowAgainButton.setMaxWidth(Double.MAX_VALUE); @@ -932,6 +941,19 @@ private void activateDisplayPreferences() { sortMarketCurrenciesNumerically.setSelected(preferences.isSortMarketCurrenciesNumerically()); sortMarketCurrenciesNumerically.setOnAction(e -> preferences.setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically.isSelected())); + boolean disableToggle = false; + if (user.getPaymentAccounts() != null) { + Set supportedPaymentMethods = user.getPaymentAccounts().stream() + .map(PaymentAccount::getPaymentMethod).collect(Collectors.toSet()); + disableToggle = supportedPaymentMethods.isEmpty(); + } + hideNonAccountPaymentMethodsToggle.setSelected(preferences.isHideNonAccountPaymentMethods() && !disableToggle); + hideNonAccountPaymentMethodsToggle.setOnAction(e -> preferences.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethodsToggle.isSelected())); + hideNonAccountPaymentMethodsToggle.setDisable(disableToggle); + + denyApiTakerToggle.setSelected(preferences.isDenyApiTaker()); + denyApiTakerToggle.setOnAction(e -> preferences.setDenyApiTaker(denyApiTakerToggle.isSelected())); + resetDontShowAgainButton.setOnAction(e -> preferences.resetDontShowAgain()); editCustomBtcExplorer.setOnAction(e -> { @@ -1108,8 +1130,9 @@ private void deactivateDisplayCurrencies() { private void deactivateDisplayPreferences() { useAnimations.setOnAction(null); useDarkMode.setOnAction(null); - // useStickyMarketPriceCheckBox.setOnAction(null); sortMarketCurrenciesNumerically.setOnAction(null); + hideNonAccountPaymentMethodsToggle.setOnAction(null); + denyApiTakerToggle.setOnAction(null); showOwnOffersInOfferBook.setOnAction(null); resetDontShowAgainButton.setOnAction(null); if (displayStandbyModeFeature) { diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index 39044f8d526..dc48c8c5459 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -239,7 +239,7 @@ public void testMaxCharactersForAmountWithNoOffes() { 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 void testMaxCharactersForAmount() { 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 void testMaxCharactersForAmountRange() { 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 void testMaxCharactersForVolumeWithNoOffes() { 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 void testMaxCharactersForVolume() { 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 void testMaxCharactersForVolumeRange() { 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 void testMaxCharactersForPriceWithNoOffers() { 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 void testMaxCharactersForPrice() { 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 void testMaxCharactersForPriceDistanceWithNoOffers() { 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 void testMaxCharactersForPriceDistance() { 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 void testGetPrice() { when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true)); final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null, - null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); + null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter()); final OfferBookListItem item = make(btcBuyItem.but( with(useMarketBasedPrice, true), diff --git a/inventory/src/main/java/bisq/inventory/InventoryMonitor.java b/inventory/src/main/java/bisq/inventory/InventoryMonitor.java index 4eacd79991f..6da6e63d3e5 100644 --- a/inventory/src/main/java/bisq/inventory/InventoryMonitor.java +++ b/inventory/src/main/java/bisq/inventory/InventoryMonitor.java @@ -257,6 +257,7 @@ private NetworkNode getNetworkNode(File torDir) { CoreNetworkProtoResolver networkProtoResolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone()); return new NetworkNodeProvider(networkProtoResolver, ArrayList::new, + null, useLocalhostForP2P, 9999, torDir, diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java index 8c3eab2e637..48c6089776e 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java @@ -116,7 +116,7 @@ protected void execute() { // start the network node networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9053")), new CoreNetworkProtoResolver(Clock.systemDefaultZone()), false, - new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName())); + new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName()), null); networkNode.start(this); // wait for the HS to be published diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java index fa37505fb5d..9550b3dae9f 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java @@ -155,7 +155,7 @@ protected void execute() { // start the network node final NetworkNode networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9054")), new CoreNetworkProtoResolver(Clock.systemDefaultZone()), false, - new AvailableTor(Monitor.TOR_WORKING_DIR, "unused")); + new AvailableTor(Monitor.TOR_WORKING_DIR, "unused"), null); // we do not need to start the networkNode, as we do not need the HS //networkNode.start(this); diff --git a/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java b/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java index 078db3b21e5..e8cf8d92385 100644 --- a/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java +++ b/p2p/src/main/java/bisq/network/p2p/NetworkNodeProvider.java @@ -19,18 +19,19 @@ import bisq.network.p2p.network.BridgeAddressProvider; import bisq.network.p2p.network.LocalhostNetworkNode; +import bisq.network.p2p.network.NetworkFilter; import bisq.network.p2p.network.NetworkNode; import bisq.network.p2p.network.NewTor; import bisq.network.p2p.network.RunningTor; +import bisq.network.p2p.network.TorMode; import bisq.network.p2p.network.TorNetworkNode; import bisq.common.config.Config; import bisq.common.proto.network.NetworkProtoResolver; -import javax.inject.Provider; -import javax.inject.Named; - import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; import java.io.File; @@ -43,6 +44,7 @@ public class NetworkNodeProvider implements Provider { @Inject public NetworkNodeProvider(NetworkProtoResolver networkProtoResolver, BridgeAddressProvider bridgeAddressProvider, + @Nullable NetworkFilter networkFilter, @Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P, @Named(Config.NODE_PORT) int port, @Named(Config.TOR_DIR) File torDir, @@ -52,13 +54,33 @@ public NetworkNodeProvider(NetworkProtoResolver networkProtoResolver, @Named(Config.TOR_CONTROL_PASSWORD) String password, @Nullable @Named(Config.TOR_CONTROL_COOKIE_FILE) File cookieFile, @Named(Config.TOR_STREAM_ISOLATION) boolean streamIsolation, - @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication ) { - networkNode = useLocalhostForP2P ? - new LocalhostNetworkNode(port, networkProtoResolver) : - new TorNetworkNode(port, networkProtoResolver, streamIsolation, - controlPort != Config.UNSPECIFIED_PORT ? - new RunningTor(torDir, controlPort, password, cookieFile, useSafeCookieAuthentication) : - new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider.getBridgeAddresses())); + @Named(Config.TOR_CONTROL_USE_SAFE_COOKIE_AUTH) boolean useSafeCookieAuthentication) { + if (useLocalhostForP2P) { + networkNode = new LocalhostNetworkNode(port, networkProtoResolver, networkFilter); + } else { + TorMode torMode = getTorMode(bridgeAddressProvider, + torDir, + torrcFile, + torrcOptions, + controlPort, + password, + cookieFile, + useSafeCookieAuthentication); + networkNode = new TorNetworkNode(port, networkProtoResolver, streamIsolation, torMode, networkFilter); + } + } + + private TorMode getTorMode(BridgeAddressProvider bridgeAddressProvider, + File torDir, + @Nullable File torrcFile, + String torrcOptions, + int controlPort, + String password, + @Nullable File cookieFile, + boolean useSafeCookieAuthentication) { + return controlPort != Config.UNSPECIFIED_PORT ? + new RunningTor(torDir, controlPort, password, cookieFile, useSafeCookieAuthentication) : + new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider.getBridgeAddresses()); } @Override diff --git a/p2p/src/main/java/bisq/network/p2p/P2PModule.java b/p2p/src/main/java/bisq/network/p2p/P2PModule.java index 896fe8098c2..e5df2c6e802 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PModule.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PModule.java @@ -22,7 +22,6 @@ import bisq.network.http.HttpClientImpl; import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.peers.BanList; import bisq.network.p2p.peers.Broadcaster; import bisq.network.p2p.peers.PeerManager; import bisq.network.p2p.peers.getdata.RequestDataManager; @@ -68,7 +67,6 @@ protected void configure() { bind(PeerExchangeManager.class).in(Singleton.class); bind(KeepAliveManager.class).in(Singleton.class); bind(Broadcaster.class).in(Singleton.class); - bind(BanList.class).in(Singleton.class); bind(NetworkNode.class).toProvider(NetworkNodeProvider.class).in(Singleton.class); bind(Socks5ProxyProvider.class).in(Singleton.class); bind(HttpClient.class).to(HttpClientImpl.class); diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index 0bec36f8ac5..f6a1ed15e8b 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -24,7 +24,6 @@ import bisq.network.p2p.PrefixedSealedAndSignedMessage; import bisq.network.p2p.SendersNodeAddressMessage; import bisq.network.p2p.SupportedCapabilitiesMessage; -import bisq.network.p2p.peers.BanList; import bisq.network.p2p.peers.getdata.messages.GetDataRequest; import bisq.network.p2p.peers.getdata.messages.GetDataResponse; import bisq.network.p2p.peers.keepalive.messages.KeepAliveMessage; @@ -141,6 +140,8 @@ public static int getPermittedMessageSize() { private final Socket socket; // private final MessageListener messageListener; private final ConnectionListener connectionListener; + @Nullable + private final NetworkFilter networkFilter; @Getter private final String uid; private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "Connection.java executor-service")); @@ -184,9 +185,11 @@ public static int getPermittedMessageSize() { MessageListener messageListener, ConnectionListener connectionListener, @Nullable NodeAddress peersNodeAddress, - NetworkProtoResolver networkProtoResolver) { + NetworkProtoResolver networkProtoResolver, + @Nullable NetworkFilter networkFilter) { this.socket = socket; this.connectionListener = connectionListener; + this.networkFilter = networkFilter; uid = UUID.randomUUID().toString(); statistic = new Statistic(); @@ -209,9 +212,12 @@ private void init(@Nullable NodeAddress peersNodeAddress) { // We create a thread for handling inputStream data singleThreadExecutor.submit(this); - if (peersNodeAddress != null) + if (peersNodeAddress != null) { setPeersNodeAddress(peersNodeAddress); - + if (networkFilter != null && networkFilter.isPeerBanned(peersNodeAddress)) { + reportInvalidRequest(RuleViolation.PEER_BANNED); + } + } UserThread.execute(() -> connectionListener.onConnection(this)); } catch (Throwable e) { handleException(e); @@ -235,88 +241,98 @@ public Capabilities getCapabilities() { public void sendMessage(NetworkEnvelope networkEnvelope) { log.debug(">> Send networkEnvelope of type: {}", networkEnvelope.getClass().getSimpleName()); - if (!stopped) { - if (noCapabilityRequiredOrCapabilityIsSupported(networkEnvelope)) { - try { - String peersNodeAddress = peersNodeAddressOptional.map(NodeAddress::toString).orElse("null"); - - if (networkEnvelope instanceof PrefixedSealedAndSignedMessage && peersNodeAddressOptional.isPresent()) { - setPeerType(Connection.PeerType.DIRECT_MSG_PEER); - - log.debug("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + - "Sending direct message to peer" + - "Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" + - "\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n", - peersNodeAddress, uid, Utilities.toTruncatedString(networkEnvelope), -1); - } else if (networkEnvelope instanceof GetDataResponse && ((GetDataResponse) networkEnvelope).isGetUpdatedDataResponse()) { - setPeerType(Connection.PeerType.PEER); - } + if (stopped) { + log.debug("called sendMessage but was already stopped"); + return; + } - // Throttle outbound network_messages - long now = System.currentTimeMillis(); - long elapsed = now - lastSendTimeStamp; - if (elapsed < getSendMsgThrottleTrigger()) { - log.debug("We got 2 sendMessage requests in less than {} ms. We set the thread to sleep " + - "for {} ms to avoid flooding our peer. lastSendTimeStamp={}, now={}, elapsed={}, networkEnvelope={}", - getSendMsgThrottleTrigger(), getSendMsgThrottleSleep(), lastSendTimeStamp, now, elapsed, - networkEnvelope.getClass().getSimpleName()); - - // check if BundleOfEnvelopes is supported - if (getCapabilities().containsAll(new Capabilities(Capability.BUNDLE_OF_ENVELOPES))) { - synchronized (lock) { - // check if current envelope fits size - // - no? create new envelope - if (queueOfBundles.isEmpty() || queueOfBundles.element().toProtoNetworkEnvelope().getSerializedSize() + networkEnvelope.toProtoNetworkEnvelope().getSerializedSize() > MAX_PERMITTED_MESSAGE_SIZE * 0.9) { - // - no? create a bucket - queueOfBundles.add(new BundleOfEnvelopes()); - - // - and schedule it for sending - lastSendTimeStamp += getSendMsgThrottleSleep(); - - bundleSender.schedule(() -> { - if (!stopped) { - synchronized (lock) { - BundleOfEnvelopes bundle = queueOfBundles.poll(); - if (bundle != null && !stopped) { - NetworkEnvelope envelope = bundle.getEnvelopes().size() == 1 ? - bundle.getEnvelopes().get(0) : - bundle; - try { - protoOutputStream.writeEnvelope(envelope); - } catch (Throwable t) { - log.error("Sending envelope of class {} to address {} " + - "failed due {}", - envelope.getClass().getSimpleName(), - this.getPeersNodeAddressOptional(), - t.toString()); - log.error("envelope: {}", envelope); - } - } + if (networkFilter != null && + peersNodeAddressOptional.isPresent() && + networkFilter.isPeerBanned(peersNodeAddressOptional.get())) { + reportInvalidRequest(RuleViolation.PEER_BANNED); + return; + } + + if (!noCapabilityRequiredOrCapabilityIsSupported(networkEnvelope)) { + log.debug("Capability for networkEnvelope is required but not supported"); + return; + } + + try { + if (networkEnvelope instanceof PrefixedSealedAndSignedMessage && peersNodeAddressOptional.isPresent()) { + setPeerType(Connection.PeerType.DIRECT_MSG_PEER); + + log.debug("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + + "Sending direct message to peer" + + "Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" + + "\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n", + peersNodeAddressOptional.map(NodeAddress::toString).orElse("null"), + uid, Utilities.toTruncatedString(networkEnvelope), -1); + } else if (networkEnvelope instanceof GetDataResponse && ((GetDataResponse) networkEnvelope).isGetUpdatedDataResponse()) { + setPeerType(Connection.PeerType.PEER); + } + + // Throttle outbound network_messages + long now = System.currentTimeMillis(); + long elapsed = now - lastSendTimeStamp; + if (elapsed < getSendMsgThrottleTrigger()) { + log.debug("We got 2 sendMessage requests in less than {} ms. We set the thread to sleep " + + "for {} ms to avoid flooding our peer. lastSendTimeStamp={}, now={}, elapsed={}, networkEnvelope={}", + getSendMsgThrottleTrigger(), getSendMsgThrottleSleep(), lastSendTimeStamp, now, elapsed, + networkEnvelope.getClass().getSimpleName()); + + // check if BundleOfEnvelopes is supported + if (getCapabilities().containsAll(new Capabilities(Capability.BUNDLE_OF_ENVELOPES))) { + synchronized (lock) { + // check if current envelope fits size + // - no? create new envelope + if (queueOfBundles.isEmpty() || queueOfBundles.element().toProtoNetworkEnvelope().getSerializedSize() + networkEnvelope.toProtoNetworkEnvelope().getSerializedSize() > MAX_PERMITTED_MESSAGE_SIZE * 0.9) { + // - no? create a bucket + queueOfBundles.add(new BundleOfEnvelopes()); + + // - and schedule it for sending + lastSendTimeStamp += getSendMsgThrottleSleep(); + + bundleSender.schedule(() -> { + if (!stopped) { + synchronized (lock) { + BundleOfEnvelopes bundle = queueOfBundles.poll(); + if (bundle != null && !stopped) { + NetworkEnvelope envelope = bundle.getEnvelopes().size() == 1 ? + bundle.getEnvelopes().get(0) : + bundle; + try { + protoOutputStream.writeEnvelope(envelope); + } catch (Throwable t) { + log.error("Sending envelope of class {} to address {} " + + "failed due {}", + envelope.getClass().getSimpleName(), + this.getPeersNodeAddressOptional(), + t.toString()); + log.error("envelope: {}", envelope); } } - }, lastSendTimeStamp - now, TimeUnit.MILLISECONDS); + } } - - // - yes? add to bucket - queueOfBundles.element().add(networkEnvelope); - } - return; + }, lastSendTimeStamp - now, TimeUnit.MILLISECONDS); } - Thread.sleep(getSendMsgThrottleSleep()); + // - yes? add to bucket + queueOfBundles.element().add(networkEnvelope); } + return; + } - lastSendTimeStamp = now; + Thread.sleep(getSendMsgThrottleSleep()); + } - if (!stopped) { - protoOutputStream.writeEnvelope(networkEnvelope); - } - } catch (Throwable t) { - handleException(t); - } + lastSendTimeStamp = now; + + if (!stopped) { + protoOutputStream.writeEnvelope(networkEnvelope); } - } else { - log.debug("called sendMessage but was already stopped"); + } catch (Throwable t) { + handleException(t); } } @@ -484,13 +500,9 @@ private void setPeersNodeAddress(NodeAddress peerNodeAddress) { } peersNodeAddressProperty.set(peerNodeAddress); - - if (BanList.isBanned(peerNodeAddress)) { - log.warn("We detected a connection to a banned peer. We will close that connection. (setPeersNodeAddress)"); - reportInvalidRequest(RuleViolation.PEER_BANNED); - } } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -738,6 +750,13 @@ public void run() { return; } + if (networkFilter != null && + peersNodeAddressOptional.isPresent() && + networkFilter.isPeerBanned(peersNodeAddressOptional.get())) { + reportInvalidRequest(RuleViolation.PEER_BANNED); + return; + } + // Throttle inbound network_messages long now = System.currentTimeMillis(); long elapsed = now - lastReadTimeStamp; @@ -841,11 +860,10 @@ && reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID)) { "connection={}", proto.getCloseConnectionMessage().getReason(), this); if (CloseConnectionReason.PEER_BANNED.name().equals(proto.getCloseConnectionMessage().getReason())) { - log.warn("We got shut down because we are banned by the other peer. (InputHandler.run CloseConnectionMessage)"); - shutDown(CloseConnectionReason.PEER_BANNED); - } else { - shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER); + log.warn("We got shut down because we are banned by the other peer. " + + "(InputHandler.run CloseConnectionMessage). Peer: {}", getPeersNodeAddressOptional()); } + shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER); return; } else if (!stopped) { // We don't want to get the activity ts updated by ping/pong msg @@ -883,6 +901,11 @@ && reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID)) { // We check for a banned peer inside setPeersNodeAddress() and shut down if banned. setPeersNodeAddress(senderNodeAddress); } + + if (networkFilter != null && networkFilter.isPeerBanned(senderNodeAddress)) { + reportInvalidRequest(RuleViolation.PEER_BANNED); + return; + } } } diff --git a/p2p/src/main/java/bisq/network/p2p/network/InboundConnection.java b/p2p/src/main/java/bisq/network/p2p/network/InboundConnection.java index 1e6948fab1e..b691d906d2b 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/InboundConnection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/InboundConnection.java @@ -21,11 +21,14 @@ import java.net.Socket; +import org.jetbrains.annotations.Nullable; + public class InboundConnection extends Connection { public InboundConnection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener, - NetworkProtoResolver networkProtoResolver) { - super(socket, messageListener, connectionListener, null, networkProtoResolver); + NetworkProtoResolver networkProtoResolver, + @Nullable NetworkFilter networkFilter) { + super(socket, messageListener, connectionListener, null, networkProtoResolver, networkFilter); } } diff --git a/p2p/src/main/java/bisq/network/p2p/network/LocalhostNetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/LocalhostNetworkNode.java index b49bff1e737..97fce6e14f8 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/LocalhostNetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/LocalhostNetworkNode.java @@ -54,8 +54,10 @@ public static void setSimulateTorDelayHiddenService(int simulateTorDelayHiddenSe // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public LocalhostNetworkNode(int port, NetworkProtoResolver networkProtoResolver) { - super(port, networkProtoResolver); + public LocalhostNetworkNode(int port, + NetworkProtoResolver networkProtoResolver, + @Nullable NetworkFilter networkFilter) { + super(port, networkProtoResolver, networkFilter); } @Override diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java new file mode 100644 index 00000000000..3dcf040e21f --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkFilter.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.network; + +import bisq.network.p2p.NodeAddress; + +import java.util.function.Function; + +public interface NetworkFilter { + boolean isPeerBanned(NodeAddress nodeAddress); + + void setBannedNodeFunction(Function isNodeAddressBanned); +} diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index c2695833f7a..ae32233bc1b 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -69,6 +69,8 @@ public abstract class NetworkNode implements MessageListener { final int servicePort; private final NetworkProtoResolver networkProtoResolver; + @Nullable + private final NetworkFilter networkFilter; private final CopyOnWriteArraySet inBoundConnections = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet messageListeners = new CopyOnWriteArraySet<>(); @@ -87,9 +89,12 @@ public abstract class NetworkNode implements MessageListener { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - NetworkNode(int servicePort, NetworkProtoResolver networkProtoResolver) { + NetworkNode(int servicePort, + NetworkProtoResolver networkProtoResolver, + @Nullable NetworkFilter networkFilter) { this.servicePort = servicePort; this.networkProtoResolver = networkProtoResolver; + this.networkFilter = networkFilter; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -190,7 +195,8 @@ public void onError(Throwable throwable) { NetworkNode.this, connectionListener, peersNodeAddress, - networkProtoResolver); + networkProtoResolver, + networkFilter); if (log.isDebugEnabled()) { log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + @@ -457,7 +463,8 @@ public void onError(Throwable throwable) { server = new Server(serverSocket, NetworkNode.this, connectionListener, - networkProtoResolver); + networkProtoResolver, + networkFilter); executorService.submit(server); } diff --git a/p2p/src/main/java/bisq/network/p2p/network/OutboundConnection.java b/p2p/src/main/java/bisq/network/p2p/network/OutboundConnection.java index 5b27d02e2d5..6c6c6d341a8 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/OutboundConnection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/OutboundConnection.java @@ -23,12 +23,15 @@ import java.net.Socket; +import org.jetbrains.annotations.Nullable; + public class OutboundConnection extends Connection { public OutboundConnection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener, NodeAddress peersNodeAddress, - NetworkProtoResolver networkProtoResolver) { - super(socket, messageListener, connectionListener, peersNodeAddress, networkProtoResolver); + NetworkProtoResolver networkProtoResolver, + @Nullable NetworkFilter networkFilter) { + super(socket, messageListener, connectionListener, peersNodeAddress, networkProtoResolver, networkFilter); } } diff --git a/p2p/src/main/java/bisq/network/p2p/network/Server.java b/p2p/src/main/java/bisq/network/p2p/network/Server.java index 8f8db624ebd..f44622e6025 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Server.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Server.java @@ -31,12 +31,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.jetbrains.annotations.Nullable; + // Runs in UserThread class Server implements Runnable { private static final Logger log = LoggerFactory.getLogger(Server.class); private final MessageListener messageListener; private final ConnectionListener connectionListener; + @Nullable + private final NetworkFilter networkFilter; // accessed from different threads private final ServerSocket serverSocket; @@ -48,11 +52,13 @@ class Server implements Runnable { public Server(ServerSocket serverSocket, MessageListener messageListener, ConnectionListener connectionListener, - NetworkProtoResolver networkProtoResolver) { + NetworkProtoResolver networkProtoResolver, + @Nullable NetworkFilter networkFilter) { this.networkProtoResolver = networkProtoResolver; this.serverSocket = serverSocket; this.messageListener = messageListener; this.connectionListener = connectionListener; + this.networkFilter = networkFilter; } @Override @@ -69,7 +75,8 @@ public void run() { InboundConnection connection = new InboundConnection(socket, messageListener, connectionListener, - networkProtoResolver); + networkProtoResolver, + networkFilter); log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "Server created new inbound connection:" diff --git a/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java index 2ca0cd5b781..a57e5404bc1 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/TorNetworkNode.java @@ -87,9 +87,12 @@ public class TorNetworkNode extends NetworkNode { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public TorNetworkNode(int servicePort, NetworkProtoResolver networkProtoResolver, boolean useStreamIsolation, - TorMode torMode) { - super(servicePort, networkProtoResolver); + public TorNetworkNode(int servicePort, + NetworkProtoResolver networkProtoResolver, + boolean useStreamIsolation, + TorMode torMode, + @Nullable NetworkFilter networkFilter) { + super(servicePort, networkProtoResolver, networkFilter); this.torMode = torMode; this.streamIsolation = useStreamIsolation; createExecutorService(); diff --git a/p2p/src/main/java/bisq/network/p2p/peers/BanList.java b/p2p/src/main/java/bisq/network/p2p/peers/BanList.java deleted file mode 100644 index 212a715a95a..00000000000 --- a/p2p/src/main/java/bisq/network/p2p/peers/BanList.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.network.p2p.peers; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.config.Config; - -import javax.inject.Named; - -import javax.inject.Inject; - -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -public class BanList { - private static List list = new ArrayList<>(); - - public static void add(NodeAddress onionAddress) { - list.add(onionAddress); - } - - public static boolean isBanned(NodeAddress nodeAddress) { - return list.contains(nodeAddress); - } - - @Inject - public BanList(@Named(Config.BAN_LIST) List banList) { - if (!banList.isEmpty()) - BanList.list = banList.stream().map(NodeAddress::new).collect(Collectors.toList()); - } -} diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java index 34620b53fd2..999c750c176 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/RequestDataHandler.java @@ -32,10 +32,10 @@ import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.proto.network.NetworkPayload; import bisq.common.util.Tuple2; import bisq.common.util.Utilities; -import com.google.common.collect.Streams; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; @@ -43,7 +43,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -232,20 +231,15 @@ public void stop() { private void logContents(GetDataResponse getDataResponse) { Set dataSet = getDataResponse.getDataSet(); Set persistableNetworkPayloadSet = getDataResponse.getPersistableNetworkPayloadSet(); - Map> numPayloadsByClassName = new HashMap<>(); - Streams.concat(dataSet.stream().map(ProtectedStorageEntry::getProtectedStoragePayload).filter(Objects::nonNull), - persistableNetworkPayloadSet.stream()) - .forEach(data -> { - String className = data.getClass().getSimpleName(); - // The data.toProtoMessage().getSerializedSize() call is not cheap, so want to avoid to call it on - // each object. As most objects of the same data type are expected to have a similar size, - // we only take the first and multiply later to get the total size. - // This is sufficient for the informational purpose of that log. - numPayloadsByClassName.putIfAbsent(className, new Tuple2<>(new AtomicInteger(0), - data.toProtoMessage().getSerializedSize())); - numPayloadsByClassName.get(className).first.getAndIncrement(); - - }); + Map> numPayloadsByClassName = new HashMap<>(); + dataSet.forEach(protectedStorageEntry -> { + String className = protectedStorageEntry.getProtectedStoragePayload().getClass().getSimpleName(); + addDetails(numPayloadsByClassName, protectedStorageEntry, className); + }); + persistableNetworkPayloadSet.forEach(persistableNetworkPayload -> { + String className = persistableNetworkPayload.getClass().getSimpleName(); + addDetails(numPayloadsByClassName, persistableNetworkPayload, className); + }); StringBuilder sb = new StringBuilder(); String sep = System.lineSeparator(); sb.append(sep).append("#################################################################").append(sep); @@ -256,13 +250,24 @@ private void logContents(GetDataResponse getDataResponse) { numPayloadsByClassName.forEach((key, value) -> sb.append(key) .append(": ") .append(value.first.get()) - .append(" / ≈") - .append(Utilities.readableFileSize(value.second * value.first.get())) + .append(" / ") + .append(Utilities.readableFileSize(value.second.get())) .append(sep)); sb.append("#################################################################"); log.info(sb.toString()); } + private void addDetails(Map> numPayloadsByClassName, + NetworkPayload networkPayload, String className) { + numPayloadsByClassName.putIfAbsent(className, new Tuple2<>(new AtomicInteger(0), + new AtomicInteger(0))); + numPayloadsByClassName.get(className).first.getAndIncrement(); + // toProtoMessage().getSerializedSize() is not very cheap. For about 1500 objects it takes about 20 ms + // I think its justified to get accurate metrics but if it turns out to be a performance issue we might need + // to remove it and use some more rough estimation by taking only the size of one data type and multiply it. + numPayloadsByClassName.get(className).second.getAndAdd(networkPayload.toProtoMessage().getSerializedSize()); + } + @SuppressWarnings("UnusedParameters") private void handleFault(String errorMessage, NodeAddress nodeAddress, diff --git a/p2p/src/main/java/bisq/network/p2p/peers/getdata/messages/GetDataResponse.java b/p2p/src/main/java/bisq/network/p2p/peers/getdata/messages/GetDataResponse.java index a86d6f27834..255c8c13474 100644 --- a/p2p/src/main/java/bisq/network/p2p/peers/getdata/messages/GetDataResponse.java +++ b/p2p/src/main/java/bisq/network/p2p/peers/getdata/messages/GetDataResponse.java @@ -27,8 +27,8 @@ import bisq.common.app.Version; import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.network.NetworkProtoResolver; +import bisq.common.util.Utilities; -import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @@ -107,24 +107,18 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { protobuf.NetworkEnvelope proto = getNetworkEnvelopeBuilder() .setGetDataResponse(builder) .build(); - log.info("Sending a GetDataResponse with {} kB", proto.getSerializedSize() / 1000d); + log.info("Sending a GetDataResponse with {}", Utilities.readableFileSize(proto.getSerializedSize())); return proto; } public static GetDataResponse fromProto(protobuf.GetDataResponse proto, NetworkProtoResolver resolver, int messageVersion) { - log.info("Received a GetDataResponse with {} kB", proto.getSerializedSize() / 1000d); - Set dataSet = new HashSet<>( - proto.getDataSetList().stream() - .map(entry -> (ProtectedStorageEntry) resolver.fromProto(entry)) - .collect(Collectors.toSet())); - - Set persistableNetworkPayloadSet = new HashSet<>( - proto.getPersistableNetworkPayloadItemsList().stream() - .map(e -> (PersistableNetworkPayload) resolver.fromProto(e)) - .collect(Collectors.toSet())); - + log.info("Received a GetDataResponse with {}", Utilities.readableFileSize(proto.getSerializedSize())); + Set dataSet = proto.getDataSetList().stream() + .map(entry -> (ProtectedStorageEntry) resolver.fromProto(entry)).collect(Collectors.toSet()); + Set persistableNetworkPayloadSet = proto.getPersistableNetworkPayloadItemsList().stream() + .map(e -> (PersistableNetworkPayload) resolver.fromProto(e)).collect(Collectors.toSet()); return new GetDataResponse(dataSet, persistableNetworkPayloadSet, proto.getRequestNonce(), diff --git a/p2p/src/test/java/bisq/network/p2p/DummySeedNode.java b/p2p/src/test/java/bisq/network/p2p/DummySeedNode.java index 3fe5205c661..6bd65596c6c 100644 --- a/p2p/src/test/java/bisq/network/p2p/DummySeedNode.java +++ b/p2p/src/test/java/bisq/network/p2p/DummySeedNode.java @@ -17,8 +17,6 @@ package bisq.network.p2p; -import bisq.network.p2p.peers.BanList; - import bisq.common.UserThread; import bisq.common.app.Log; import bisq.common.app.Version; @@ -140,7 +138,6 @@ public void processArgs(String[] args) { list.forEach(e -> { checkArgument(e.contains(":") && e.split(":").length == 2 && e.split(":")[1].length() == 4, "Wrong program argument " + e); - BanList.add(new NodeAddress(e)); }); log.debug("From processArgs: ignoreList=" + list); } else if (arg.startsWith(HELP)) { diff --git a/p2p/src/test/java/bisq/network/p2p/network/LocalhostNetworkNodeTest.java b/p2p/src/test/java/bisq/network/p2p/network/LocalhostNetworkNodeTest.java index 1a09281e0d6..8315332c34f 100644 --- a/p2p/src/test/java/bisq/network/p2p/network/LocalhostNetworkNodeTest.java +++ b/p2p/src/test/java/bisq/network/p2p/network/LocalhostNetworkNodeTest.java @@ -38,13 +38,10 @@ public class LocalhostNetworkNodeTest { private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNodeTest.class); - - - @Test public void testMessage() throws InterruptedException, IOException { CountDownLatch msgLatch = new CountDownLatch(2); - LocalhostNetworkNode node1 = new LocalhostNetworkNode(9001, TestUtils.getNetworkProtoResolver()); + LocalhostNetworkNode node1 = new LocalhostNetworkNode(9001, TestUtils.getNetworkProtoResolver(), null); node1.addMessageListener((message, connection) -> { log.debug("onMessage node1 " + message); msgLatch.countDown(); @@ -72,7 +69,7 @@ public void onRequestCustomBridges() { } }); - LocalhostNetworkNode node2 = new LocalhostNetworkNode(9002, TestUtils.getNetworkProtoResolver()); + LocalhostNetworkNode node2 = new LocalhostNetworkNode(9002, TestUtils.getNetworkProtoResolver(), null); node2.addMessageListener((message, connection) -> { log.debug("onMessage node2 " + message); msgLatch.countDown(); diff --git a/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java b/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java index b050fdf6596..3b544da4e59 100644 --- a/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java +++ b/p2p/src/test/java/bisq/network/p2p/network/TorNetworkNodeTest.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.IOException; + import java.util.ArrayList; import java.util.concurrent.CountDownLatch; @@ -49,13 +50,12 @@ public class TorNetworkNodeTest { private CountDownLatch latch; - @Test public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException { latch = new CountDownLatch(1); int port = 9001; TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false, - new NewTor(new File("torNode_" + port), null, "", new ArrayList())); + new NewTor(new File("torNode_" + port), null, "", new ArrayList()), null); node1.start(new SetupListener() { @Override public void onTorNodeReady() { @@ -82,7 +82,7 @@ public void onRequestCustomBridges() { latch = new CountDownLatch(1); int port2 = 9002; TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false, - new NewTor(new File("torNode_" + port), null, "", new ArrayList())); + new NewTor(new File("torNode_" + port), null, "", new ArrayList()), null); node2.start(new SetupListener() { @Override public void onTorNodeReady() { @@ -140,7 +140,7 @@ public void testTorNodeAfterBothReady() throws InterruptedException, IOException latch = new CountDownLatch(2); int port = 9001; TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false, - new NewTor(new File("torNode_" + port), null, "", new ArrayList())); + new NewTor(new File("torNode_" + port), null, "", new ArrayList()), null); node1.start(new SetupListener() { @Override public void onTorNodeReady() { @@ -166,7 +166,7 @@ public void onRequestCustomBridges() { int port2 = 9002; TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false, - new NewTor(new File("torNode_" + port), null, "", new ArrayList())); + new NewTor(new File("torNode_" + port), null, "", new ArrayList()), null); node2.start(new SetupListener() { @Override public void onTorNodeReady() { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 66af295531f..49681858e1b 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -158,6 +158,7 @@ message OfferAvailabilityRequest { int64 takers_trade_price = 3; repeated int32 supported_capabilities = 4; string uid = 5; + bool is_taker_api_user = 6; } message OfferAvailabilityResponse { @@ -652,7 +653,7 @@ message RefundAgent { } message Filter { - repeated string banned_node_address = 1; + repeated string node_addresses_banned_from_trading = 1; repeated string banned_offer_ids = 2; repeated PaymentAccountFilter banned_payment_accounts = 3; string signature_as_base64 = 4; @@ -677,6 +678,8 @@ message Filter { repeated string bannedPrivilegedDevPubKeys = 23; bool disable_auto_conf = 24; repeated string banned_auto_conf_explorers = 25; + repeated string node_addresses_banned_from_network = 26; + bool disable_api = 27; } // Deprecated @@ -912,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; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1613,6 +1618,9 @@ message PreferencesPayload { bool tac_accepted_v120 = 55; repeated AutoConfirmSettings auto_confirm_settings = 56; double bsq_average_trim_threshold = 57; + bool hide_non_account_payment_methods = 58; + bool show_offers_matching_my_accounts = 59; + bool deny_api_taker = 60; } message AutoConfirmSettings {