From b36802dc49661ebe3669d5145644bdeb4ed01781 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 22 Dec 2020 19:03:09 -0500 Subject: [PATCH 01/16] Add percentage price to open offer view --- .../java/bisq/desktop/main/PriceUtil.java | 151 ++++++++++++++++++ .../offer/offerbook/OfferBookViewModel.java | 98 +----------- .../portfolio/openoffer/OpenOffersView.java | 8 +- .../openoffer/OpenOffersViewModel.java | 32 ++-- 4 files changed, 184 insertions(+), 105 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/PriceUtil.java diff --git a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java new file mode 100644 index 00000000000..1de6b84ab10 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java @@ -0,0 +1,151 @@ +/* + * 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.desktop.main; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.Optional; + +import javax.annotation.Nullable; + +import static bisq.desktop.main.shared.ChatView.log; +import static com.google.common.base.Preconditions.checkNotNull; + +@Singleton +public class PriceUtil { + private final PriceFeedService priceFeedService; + private final TradeStatisticsManager tradeStatisticsManager; + private final Preferences preferences; + @Nullable + private Price bsq30DayAveragePrice; + + @Inject + public PriceUtil(PriceFeedService priceFeedService, + TradeStatisticsManager tradeStatisticsManager, + Preferences preferences) { + this.priceFeedService = priceFeedService; + this.tradeStatisticsManager = tradeStatisticsManager; + this.preferences = preferences; + } + + public void recalculateBsq30DayAveragePrice() { + bsq30DayAveragePrice = null; + bsq30DayAveragePrice = getBsq30DayAveragePrice(); + } + + public Price getBsq30DayAveragePrice() { + if (bsq30DayAveragePrice == null) { + bsq30DayAveragePrice = AveragePriceUtil.getAveragePriceTuple(preferences, + tradeStatisticsManager, 30).second; + } + return bsq30DayAveragePrice; + } + + public boolean hasMarketPrice(Offer offer) { + String currencyCode = offer.getCurrencyCode(); + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + Price price = offer.getPrice(); + return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable(); + } + + public Optional getMarketBasedPrice(Offer offer, + OfferPayload.Direction direction) { + if (offer.isUseMarketBasedPrice()) { + return Optional.of(offer.getMarketPriceMargin()); + } + + if (!hasMarketPrice(offer)) { + if (offer.getCurrencyCode().equals("BSQ")) { + Price bsq30DayAveragePrice = getBsq30DayAveragePrice(); + if (bsq30DayAveragePrice.isPositive()) { + double scaled = MathUtils.scaleDownByPowerOf10(bsq30DayAveragePrice.getValue(), 8); + return calculatePercentage(offer, scaled, direction); + } else { + return Optional.empty(); + } + } else { + log.trace("We don't have a market price. " + + "That case could only happen if you don't have a price feed."); + return Optional.empty(); + } + } + + String currencyCode = offer.getCurrencyCode(); + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + double marketPriceAsDouble = checkNotNull(marketPrice).getPrice(); + return calculatePercentage(offer, marketPriceAsDouble, direction); + } + + public Optional calculatePercentage(Offer offer, + double marketPrice, + OfferPayload.Direction direction) { + // If the offer did not use % price we calculate % from current market price + String currencyCode = offer.getCurrencyCode(); + Price price = offer.getPrice(); + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + long priceAsLong = checkNotNull(price).getValue(); + double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision); + double value; + if (direction == OfferPayload.Direction.SELL) { + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + if (marketPrice == 0) { + return Optional.empty(); + } + value = 1 - scaled / marketPrice; + } else { + if (marketPrice == 1) { + return Optional.empty(); + } + value = scaled / marketPrice - 1; + } + } else { + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + if (marketPrice == 1) { + return Optional.empty(); + } + value = scaled / marketPrice - 1; + } else { + if (marketPrice == 0) { + return Optional.empty(); + } + value = 1 - scaled / marketPrice; + } + } + return Optional.of(value); + } +} 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 2216cfd8c45..98737f4036c 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 @@ -20,6 +20,7 @@ import bisq.desktop.Navigation; import bisq.desktop.common.model.ActivatableViewModel; import bisq.desktop.main.MainView; +import bisq.desktop.main.PriceUtil; import bisq.desktop.main.settings.SettingsView; import bisq.desktop.main.settings.preferences.PreferencesView; import bisq.desktop.util.DisplayUtils; @@ -35,7 +36,6 @@ import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; -import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; @@ -44,14 +44,11 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.payload.PaymentMethod; -import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.Trade; import bisq.core.trade.closed.ClosedTradableManager; -import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; import bisq.core.user.User; -import bisq.core.util.AveragePriceUtil; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; @@ -62,10 +59,8 @@ import bisq.common.app.Version; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; -import org.bitcoinj.utils.Fiat; import com.google.inject.Inject; @@ -96,10 +91,6 @@ import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j class OfferBookViewModel extends ActivatableViewModel { private final OpenOfferManager openOfferManager; @@ -113,7 +104,7 @@ class OfferBookViewModel extends ActivatableViewModel { private final FilterManager filterManager; final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; - private final TradeStatisticsManager tradeStatisticsManager; + private final PriceUtil priceUtil; private final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; @@ -139,8 +130,6 @@ class OfferBookViewModel extends ActivatableViewModel { final IntegerProperty maxPlacesForPrice = new SimpleIntegerProperty(); final IntegerProperty maxPlacesForMarketPriceMargin = new SimpleIntegerProperty(); boolean showAllPaymentMethods = true; - @Nullable - private Price bsq30DayAveragePrice; /////////////////////////////////////////////////////////////////////////////////////////// @@ -159,7 +148,7 @@ public OfferBookViewModel(User user, FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, - TradeStatisticsManager tradeStatisticsManager, + PriceUtil priceUtil, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, BsqFormatter bsqFormatter) { super(); @@ -175,7 +164,7 @@ public OfferBookViewModel(User user, this.filterManager = filterManager; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; - this.tradeStatisticsManager = tradeStatisticsManager; + this.priceUtil = priceUtil; this.btcFormatter = btcFormatter; this.bsqFormatter = bsqFormatter; @@ -239,12 +228,7 @@ protected void activate() { applyFilterPredicate(); setMarketPriceFeedCurrency(); - // Null check needed for tests passing null for tradeStatisticsManager - if (tradeStatisticsManager != null) { - bsq30DayAveragePrice = AveragePriceUtil.getAveragePriceTuple(preferences, - tradeStatisticsManager, - 30).second; - } + priceUtil.recalculateBsq30DayAveragePrice(); } @Override @@ -390,77 +374,7 @@ String getPriceAsPercentage(OfferBookListItem item) { } public Optional getMarketBasedPrice(Offer offer) { - if (offer.isUseMarketBasedPrice()) { - return Optional.of(offer.getMarketPriceMargin()); - } - - if (!hasMarketPrice(offer)) { - if (offer.getCurrencyCode().equals("BSQ")) { - if (bsq30DayAveragePrice != null && bsq30DayAveragePrice.isPositive()) { - double scaled = MathUtils.scaleDownByPowerOf10(bsq30DayAveragePrice.getValue(), 8); - return calculatePercentage(offer, scaled); - } else { - return Optional.empty(); - } - } else { - log.trace("We don't have a market price. " + - "That case could only happen if you don't have a price feed."); - return Optional.empty(); - } - } - - String currencyCode = offer.getCurrencyCode(); - checkNotNull(priceFeedService, "priceFeed must not be null"); - MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); - double marketPriceAsDouble = checkNotNull(marketPrice).getPrice(); - return calculatePercentage(offer, marketPriceAsDouble); - } - - protected Optional calculatePercentage(Offer offer, double marketPrice) { - // If the offer did not use % price we calculate % from current market price - String currencyCode = offer.getCurrencyCode(); - Price price = offer.getPrice(); - int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? - Altcoin.SMALLEST_UNIT_EXPONENT : - Fiat.SMALLEST_UNIT_EXPONENT; - long priceAsLong = checkNotNull(price).getValue(); - double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision); - - double value; - if (direction == OfferPayload.Direction.SELL) { - if (CurrencyUtil.isFiatCurrency(currencyCode)) { - if (marketPrice == 0) { - return Optional.empty(); - } - value = 1 - scaled / marketPrice; - } else { - if (marketPrice == 1) { - return Optional.empty(); - } - value = scaled / marketPrice - 1; - } - } else { - if (CurrencyUtil.isFiatCurrency(currencyCode)) { - if (marketPrice == 1) { - return Optional.empty(); - } - value = scaled / marketPrice - 1; - } else { - if (marketPrice == 0) { - return Optional.empty(); - } - value = 1 - scaled / marketPrice; - } - } - return Optional.of(value); - } - - public boolean hasMarketPrice(Offer offer) { - String currencyCode = offer.getCurrencyCode(); - checkNotNull(priceFeedService, "priceFeed must not be null"); - MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); - Price price = offer.getPrice(); - return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable(); + return priceUtil.getMarketBasedPrice(offer, direction); } String formatMarketPriceMargin(Offer offer, boolean decimalAligned) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index a6bdd858978..f1ab5965d07 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -158,9 +158,7 @@ public void initialize() { marketColumn.setComparator(Comparator.comparing(model::getMarketLabel)); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); priceColumn.setComparator(Comparator.comparing(o -> o.getOffer().getPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); - deviationColumn.setComparator(Comparator.comparing(o -> - o.getOffer().isUseMarketBasedPrice() ? o.getOffer().getMarketPriceMargin() : 1, - Comparator.nullsFirst(Comparator.naturalOrder()))); + deviationColumn.setComparator(Comparator.comparing(model::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder()))); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); @@ -504,7 +502,9 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); - setGraphic(new AutoTooltipLabel(model.getPriceDeviation(item))); + AutoTooltipLabel autoTooltipLabel = new AutoTooltipLabel(model.getPriceDeviation(item)); + autoTooltipLabel.setOpacity(item.getOffer().isUseMarketBasedPrice() ? 1 : 0.4); + setGraphic(autoTooltipLabel); } else { setGraphic(null); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 709ba4ee847..38dd5ee58cd 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -19,6 +19,7 @@ import bisq.desktop.common.model.ActivatableWithDataModel; import bisq.desktop.common.model.ViewModel; +import bisq.desktop.main.PriceUtil; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; @@ -46,6 +47,7 @@ class OpenOffersViewModel extends ActivatableWithDataModel implements ViewModel { private final P2PService p2PService; + private final PriceUtil priceUtil; private final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; @@ -53,20 +55,31 @@ class OpenOffersViewModel extends ActivatableWithDataModel @Inject public OpenOffersViewModel(OpenOffersDataModel dataModel, P2PService p2PService, + PriceUtil priceUtil, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, BsqFormatter bsqFormatter) { super(dataModel); this.p2PService = p2PService; + this.priceUtil = priceUtil; this.btcFormatter = btcFormatter; this.bsqFormatter = bsqFormatter; } - void onActivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + @Override + protected void activate() { + priceUtil.recalculateBsq30DayAveragePrice(); + } + + void onActivateOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { dataModel.onActivateOpenOffer(openOffer, resultHandler, errorMessageHandler); } - void onDeactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + void onDeactivateOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { dataModel.onDeactivateOpenOffer(openOffer, resultHandler, errorMessageHandler); } @@ -100,14 +113,15 @@ String getPrice(OpenOfferListItem item) { } String getPriceDeviation(OpenOfferListItem item) { - if ((item == null)) - return ""; Offer offer = item.getOffer(); - if (offer.isUseMarketBasedPrice()) { - return FormattingUtils.formatPercentagePrice(offer.getMarketPriceMargin()); - } else { - return Res.get("shared.na"); - } + return priceUtil.getMarketBasedPrice(offer, offer.getMirroredDirection()) + .map(FormattingUtils::formatPercentagePrice) + .orElse(""); + } + + Double getPriceDeviationAsDouble(OpenOfferListItem item) { + Offer offer = item.getOffer(); + return priceUtil.getMarketBasedPrice(offer, offer.getMirroredDirection()).orElse(0d); } String getVolume(OpenOfferListItem item) { From 8ac81d903d5da0ac6d34c76bd44258467cdcd565 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 23 Dec 2020 21:46:49 -0500 Subject: [PATCH 02/16] Refactor InfoInputTextField Use only one icon which is set on demand. We will add a new icon later so that refactoring prepares that... --- .../components/InfoInputTextField.java | 126 +++++++----------- .../desktop/main/offer/MutableOfferView.java | 2 +- 2 files changed, 51 insertions(+), 77 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java index 190e3f80c63..6d625c956f1 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java @@ -19,6 +19,7 @@ import bisq.desktop.components.controlsfx.control.PopOver; +import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.scene.Node; @@ -30,23 +31,18 @@ import lombok.Getter; -import static bisq.desktop.util.FormBuilder.getIcon; +import javax.annotation.Nullable; -public class InfoInputTextField extends AnchorPane { +import static com.google.common.base.Preconditions.checkNotNull; +public class InfoInputTextField extends AnchorPane { private final StringProperty text = new SimpleStringProperty(); - @Getter private final InputTextField inputTextField; - @Getter - private final Label infoIcon; - @Getter - private final Label warningIcon; - @Getter - private final Label privacyIcon; - - private Label currentIcon; - private PopOverWrapper popoverWrapper = new PopOverWrapper(); + private final Label icon; + private final PopOverWrapper popoverWrapper = new PopOverWrapper(); + @Nullable + private Node node; public InfoInputTextField() { this(0); @@ -56,79 +52,67 @@ public InfoInputTextField(double inputLineExtension) { super(); inputTextField = new InputTextField(inputLineExtension); - - infoIcon = getIcon(AwesomeIcon.INFO_SIGN); - infoIcon.setLayoutY(3); - infoIcon.getStyleClass().addAll("icon", "info"); - - warningIcon = getIcon(AwesomeIcon.WARNING_SIGN); - warningIcon.setLayoutY(3); - warningIcon.getStyleClass().addAll("icon", "warning"); - - privacyIcon = getIcon(AwesomeIcon.EYE_CLOSE); - privacyIcon.setLayoutY(3); - privacyIcon.getStyleClass().addAll("icon", "info"); - - AnchorPane.setLeftAnchor(infoIcon, 7.0); - AnchorPane.setLeftAnchor(warningIcon, 7.0); - AnchorPane.setLeftAnchor(privacyIcon, 7.0); AnchorPane.setRightAnchor(inputTextField, 0.0); AnchorPane.setLeftAnchor(inputTextField, 0.0); - hideIcons(); - - getChildren().addAll(inputTextField, infoIcon, warningIcon, privacyIcon); + icon = new Label(); + icon.setLayoutY(3); + AnchorPane.setLeftAnchor(icon, 7.0); + icon.setOnMouseEntered(e -> { + if (node != null) { + popoverWrapper.showPopOver(() -> checkNotNull(createPopOver())); + } + }); + icon.setOnMouseExited(e -> { + if (node != null) { + popoverWrapper.hidePopOver(); + } + }); + + hideIcon(); + + getChildren().addAll(inputTextField, icon); } - private void hideIcons() { - infoIcon.setManaged(false); - infoIcon.setVisible(false); - warningIcon.setManaged(false); - warningIcon.setVisible(false); - privacyIcon.setManaged(false); - privacyIcon.setVisible(false); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Public /////////////////////////////////////////////////////////////////////////////////////////// public void setContentForInfoPopOver(Node node) { - currentIcon = infoIcon; - - hideIcons(); - setActionHandlers(node); + setContentForPopOver(node, AwesomeIcon.INFO_SIGN); } public void setContentForWarningPopOver(Node node) { - currentIcon = warningIcon; - - hideIcons(); - setActionHandlers(node); + setContentForPopOver(node, AwesomeIcon.WARNING_SIGN, "warning"); } public void setContentForPrivacyPopOver(Node node) { - currentIcon = privacyIcon; + setContentForPopOver(node, AwesomeIcon.EYE_CLOSE); + } + + public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon) { + setContentForPopOver(node, awesomeIcon, null); + } - hideIcons(); - setActionHandlers(node); + public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon, @Nullable String style) { + this.node = node; + AwesomeDude.setIcon(icon, awesomeIcon); + icon.getStyleClass().addAll("icon", style == null ? "info" : style); + icon.setManaged(true); + icon.setVisible(true); } - public void hideInfoContent() { - currentIcon = null; - hideIcons(); + public void hideIcon() { + icon.setManaged(false); + icon.setVisible(false); } public void setIconsRightAligned() { - AnchorPane.clearConstraints(infoIcon); - AnchorPane.clearConstraints(warningIcon); - AnchorPane.clearConstraints(privacyIcon); + AnchorPane.clearConstraints(icon); AnchorPane.clearConstraints(inputTextField); - AnchorPane.setRightAnchor(infoIcon, 7.0); - AnchorPane.setRightAnchor(warningIcon, 7.0); - AnchorPane.setRightAnchor(privacyIcon, 7.0); + AnchorPane.setRightAnchor(icon, 7.0); AnchorPane.setLeftAnchor(inputTextField, 0.0); AnchorPane.setRightAnchor(inputTextField, 0.0); } @@ -146,7 +130,7 @@ public String getText() { return text.get(); } - public final StringProperty textProperty() { + public StringProperty textProperty() { return text; } @@ -155,28 +139,18 @@ public final StringProperty textProperty() { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private void setActionHandlers(Node node) { - - if (node != null) { - currentIcon.setManaged(true); - currentIcon.setVisible(true); - - // As we don't use binding here we need to recreate it on mouse over to reflect the current state - currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node))); - currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); + private PopOver createPopOver() { + if (node == null) { + return null; } - } - private PopOver createPopOver(Node node) { node.getStyleClass().add("default-text"); - PopOver popover = new PopOver(node); - if (currentIcon.getScene() != null) { + if (icon.getScene() != null) { popover.setDetachable(false); popover.setArrowLocation(PopOver.ArrowLocation.LEFT_TOP); popover.setArrowIndent(5); - - popover.show(currentIcon, -17); + popover.show(icon, -17); } return popover; } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index 78bb8dafd77..960ed61fd33 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -754,7 +754,7 @@ private void createListeners() { if (!newValue.equals("") && CurrencyUtil.isFiatCurrency(model.tradeCurrencyCode.get())) { volumeInfoInputTextField.setContentForPrivacyPopOver(createPopoverLabel(Res.get("offerbook.info.roundedFiatVolume"))); } else { - volumeInfoInputTextField.hideInfoContent(); + volumeInfoInputTextField.hideIcon(); } }; From 99192e76ae7bdd09cf23ebc7330a3922907a1927 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 23 Dec 2020 21:47:44 -0500 Subject: [PATCH 03/16] Apply formatting (no code change) --- desktop/src/main/java/bisq/desktop/util/FormBuilder.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index f91a4c83ff0..8effe07578a 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -1221,8 +1221,7 @@ public static Tuple4> addTopLabelTextFi int rowIndex, String titleTextfield, String titleCombobox - ) - { + ) { return addTopLabelTextFieldAutocompleteComboBox(gridPane, rowIndex, titleTextfield, titleCombobox, 0); } @@ -1232,8 +1231,7 @@ public static Tuple4> addTopLabelTextFi String titleTextfield, String titleCombobox, double top - ) - { + ) { HBox hBox = new HBox(); hBox.setSpacing(10); From 752984726c118a453d7dcc6975b4dd57c72edc09 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 23 Dec 2020 21:48:04 -0500 Subject: [PATCH 04/16] Add getRegularIconButton method --- desktop/src/main/java/bisq/desktop/util/FormBuilder.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index 8effe07578a..5c676b4477b 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -2143,6 +2143,14 @@ public static Button getIconButton(GlyphIcons icon, String styleClass) { return getIconButton(icon, styleClass, "2em"); } + public static Button getRegularIconButton(GlyphIcons icon) { + return getIconButton(icon, "highlight", "1.6em"); + } + + public static Button getRegularIconButton(GlyphIcons icon, String styleClass) { + return getIconButton(icon, styleClass, "1.6em"); + } + public static Button getIconButton(GlyphIcons icon, String styleClass, String iconSize) { if (icon.fontFamily().equals(MATERIAL_DESIGN_ICONS)) { Button iconButton = MaterialDesignIconFactory.get().createIconButton(icon, From 0e6b9835644a615f136518977dc314ebeca79eca Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 23 Dec 2020 21:50:53 -0500 Subject: [PATCH 05/16] Add triggerPrice --- core/src/main/java/bisq/core/offer/OpenOffer.java | 14 ++++++++++++-- proto/src/main/proto/pb.proto | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 5d9cfb4794e..64740b38db3 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -69,6 +69,11 @@ public enum State { @Nullable private NodeAddress refundAgentNodeAddress; + // Added in v1.5.3. + // If market price reaches that trigger price the offer gets deactivated + @Getter + @Setter + private long triggerPrice; public OpenOffer(Offer offer) { this.offer = offer; @@ -83,12 +88,14 @@ private OpenOffer(Offer offer, State state, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, - @Nullable NodeAddress refundAgentNodeAddress) { + @Nullable NodeAddress refundAgentNodeAddress, + long triggerPrice) { this.offer = offer; this.state = state; this.arbitratorNodeAddress = arbitratorNodeAddress; this.mediatorNodeAddress = mediatorNodeAddress; this.refundAgentNodeAddress = refundAgentNodeAddress; + this.triggerPrice = triggerPrice; if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -98,6 +105,7 @@ private OpenOffer(Offer offer, public protobuf.Tradable toProtoMessage() { protobuf.OpenOffer.Builder builder = protobuf.OpenOffer.newBuilder() .setOffer(offer.toProtoMessage()) + .setTriggerPrice(triggerPrice) .setState(protobuf.OpenOffer.State.valueOf(state.name())); Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage())); @@ -112,7 +120,8 @@ public static Tradable fromProto(protobuf.OpenOffer proto) { ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, - proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null); + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, + proto.getTriggerPrice()); } @@ -178,6 +187,7 @@ public String toString() { ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n mediatorNodeAddress=" + mediatorNodeAddress + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + + ",\n triggerPrice=" + triggerPrice + "\n}"; } } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 22d81d40de9..c804938b746 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1331,6 +1331,7 @@ message OpenOffer { NodeAddress arbitrator_node_address = 3; NodeAddress mediator_node_address = 4; NodeAddress refund_agent_node_address = 5; + int64 trigger_price = 6; } message Tradable { From 229d013844948b39db9f647f1a341d29af79e470 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 23 Dec 2020 21:52:35 -0500 Subject: [PATCH 06/16] Add PriceEventHandler --- .../bisq/core/offer/PriceEventHandler.java | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 core/src/main/java/bisq/core/offer/PriceEventHandler.java diff --git a/core/src/main/java/bisq/core/offer/PriceEventHandler.java b/core/src/main/java/bisq/core/offer/PriceEventHandler.java new file mode 100644 index 00000000000..e006c7e6bdb --- /dev/null +++ b/core/src/main/java/bisq/core/offer/PriceEventHandler.java @@ -0,0 +1,148 @@ +/* + * 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.locale.CurrencyUtil; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ListChangeListener; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.common.util.MathUtils.roundDoubleToLong; +import static bisq.common.util.MathUtils.scaleUpByPowerOf10; + +@Slf4j +@Singleton +public class PriceEventHandler { + private final OpenOfferManager openOfferManager; + private final PriceFeedService priceFeedService; + private final Map> openOffersByCurrency = new HashMap<>(); + + @Inject + public PriceEventHandler(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) { + this.openOfferManager = openOfferManager; + this.priceFeedService = priceFeedService; + } + + public void onAllServicesInitialized() { + openOfferManager.getObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + onAddedOpenOffers(c.getAddedSubList()); + } + if (c.wasRemoved()) { + onRemovedOpenOffers(c.getRemoved()); + } + }); + onAddedOpenOffers(openOfferManager.getObservableList()); + + priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> onPriceFeedChanged()); + onPriceFeedChanged(); + } + + private void onPriceFeedChanged() { + openOffersByCurrency.keySet().stream() + .map(priceFeedService::getMarketPrice) + .filter(Objects::nonNull) + .filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode())) + .forEach(marketPrice -> { + openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream() + .filter(openOffer -> !openOffer.isDeactivated()) + .forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer)); + }); + } + + private void checkPriceThreshold(bisq.core.provider.price.MarketPrice marketPrice, OpenOffer openOffer) { + Price price = openOffer.getOffer().getPrice(); + if (price == null) { + return; + } + + String currencyCode = openOffer.getOffer().getCurrencyCode(); + int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + long marketPriceAsLong = roundDoubleToLong( + scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent)); + long triggerPrice = openOffer.getTriggerPrice(); + if (triggerPrice > 0) { + OfferPayload.Direction direction = openOffer.getOffer().getDirection(); + boolean triggered = direction == OfferPayload.Direction.BUY ? + marketPriceAsLong > triggerPrice : + marketPriceAsLong < triggerPrice; + if (triggered) { + log.error("Market price exceeded the trigger price of the open offer. " + + "We deactivate the open offer with ID {}. Currency: {}; offer direction: {}; " + + "Market price: {}; Upper price threshold : {}", + openOffer.getOffer().getShortId(), + currencyCode, + direction, + marketPrice.getPrice(), + MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent) + ); + openOfferManager.deactivateOpenOffer(openOffer, () -> { + }, errorMessage -> { + }); + } + } + } + + private void onAddedOpenOffers(List openOffers) { + openOffers.forEach(openOffer -> { + String currencyCode = openOffer.getOffer().getCurrencyCode(); + openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>()); + openOffersByCurrency.get(currencyCode).add(openOffer); + + MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()); + if (marketPrice != null) { + checkPriceThreshold(marketPrice, openOffer); + } + }); + } + + private void onRemovedOpenOffers(List openOffers) { + openOffers.forEach(openOffer -> { + String currencyCode = openOffer.getOffer().getCurrencyCode(); + if (openOffersByCurrency.containsKey(currencyCode)) { + Set set = openOffersByCurrency.get(currencyCode); + set.remove(openOffer); + if (set.isEmpty()) { + openOffersByCurrency.remove(currencyCode); + } + } + }); + } +} From e5957cd2fe2d18f9a10ec5758a23b12f671f3e00 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Wed, 23 Dec 2020 23:22:02 -0500 Subject: [PATCH 07/16] Set priceTypeToggleButton invisible once we move to funds section Remove disableProperty().unbind() calls which have never got a bind call before Cleanups --- .../desktop/main/offer/MutableOfferView.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index 960ed61fd33..36d35cfdd70 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -174,6 +174,7 @@ public abstract class MutableOfferView> exten private AutoTooltipSlideToggleButton tradeFeeInBtcToggle, tradeFeeInBsqToggle; private Text xIcon, fakeXIcon; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @@ -237,7 +238,6 @@ protected void doActivate() { if (waitingForFundsSpinner != null) waitingForFundsSpinner.play(); - //directionLabel.setText(model.getDirectionLabel()); amountDescriptionLabel.setText(model.getAmountDescription()); addressTextField.setAddress(model.getAddressAsString()); addressTextField.setPaymentLabel(model.getPaymentLabel()); @@ -449,8 +449,8 @@ private void onShowPayFundsScreen() { private void updateOfferElementsStyle() { GridPane.setColumnSpan(firstRowHBox, 2); - final String activeInputStyle = "input-with-border"; - final String readOnlyInputStyle = "input-with-border-readonly"; + String activeInputStyle = "input-with-border"; + String readOnlyInputStyle = "input-with-border-readonly"; amountValueCurrencyBox.getStyleClass().remove(activeInputStyle); amountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); priceAsPercentageValueCurrencyBox.getStyleClass().remove(activeInputStyle); @@ -462,6 +462,8 @@ private void updateOfferElementsStyle() { minAmountValueCurrencyBox.getStyleClass().remove(activeInputStyle); minAmountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); + priceTypeToggleButton.setVisible(false); + resultLabel.getStyleClass().add("small"); xLabel.getStyleClass().add("small"); xIcon.setStyle(String.format("-fx-font-family: %s; -fx-font-size: %s;", MaterialDesignIcon.CLOSE.fontFamily(), "1em")); @@ -542,7 +544,6 @@ protected void close() { private void addBindings() { priceCurrencyLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getCounterCurrency(model.tradeCurrencyCode.get()), model.tradeCurrencyCode)); - marketBasedPriceLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty()); volumeCurrencyLabel.textProperty().bind(model.tradeCurrencyCode); priceDescriptionLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getPriceWithCurrencyCode(model.tradeCurrencyCode.get(), "shared.fixedPriceInCurForCur"), model.tradeCurrencyCode)); @@ -590,10 +591,6 @@ private void addBindings() { private void removeBindings() { priceCurrencyLabel.textProperty().unbind(); - fixedPriceTextField.disableProperty().unbind(); - priceCurrencyLabel.disableProperty().unbind(); - marketBasedPriceTextField.disableProperty().unbind(); - marketBasedPriceLabel.disableProperty().unbind(); volumeCurrencyLabel.textProperty().unbind(); priceDescriptionLabel.textProperty().unbind(); volumeDescriptionLabel.textProperty().unbind(); @@ -1387,7 +1384,6 @@ private void addSecondRow() { Tuple2 amountInputBoxTuple = getTradeInputBox(minAmountValueCurrencyBox, Res.get("createOffer.amountPriceBox.minAmountDescription")); - fakeXLabel = new Label(); fakeXIcon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", fakeXLabel); fakeXLabel.getStyleClass().add("opaque-icon-character"); @@ -1397,12 +1393,10 @@ private void addSecondRow() { priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); editOfferElements.add(priceTypeToggleButton); HBox.setMargin(priceTypeToggleButton, new Insets(16, 0, 0, 0)); - priceTypeToggleButton.setOnAction((actionEvent) -> updatePriceToggleButtons(model.getDataModel().getUseMarketBasedPrice().getValue())); secondRowHBox = new HBox(); - secondRowHBox.setSpacing(5); secondRowHBox.setAlignment(Pos.CENTER_LEFT); secondRowHBox.getChildren().addAll(amountInputBoxTuple.second, fakeXLabel, fixedPriceBox, priceTypeToggleButton); From bb9de0b24c94e8712868592428589119a742dbd7 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 24 Dec 2020 20:01:04 -0500 Subject: [PATCH 08/16] Add trigger price field to create offer view --- .../java/bisq/core/api/CoreOffersService.java | 2 + .../main/java/bisq/core/offer/OpenOffer.java | 8 +- .../bisq/core/offer/OpenOfferManager.java | 8 +- .../java/bisq/core/util/FormattingUtils.java | 2 +- .../resources/i18n/displayStrings.properties | 8 +- .../java/bisq/desktop/main/PriceUtil.java | 82 ++++++++++++++++ .../MobileNotificationsView.java | 33 +------ .../main/offer/MutableOfferDataModel.java | 35 ++++++- .../desktop/main/offer/MutableOfferView.java | 91 +++++++++++------ .../main/offer/MutableOfferViewModel.java | 98 +++++++++++++++++-- .../desktop/main/offer/OfferViewUtil.java | 36 +++++++ .../offer/takeoffer/TakeOfferDataModel.java | 10 +- .../main/offer/takeoffer/TakeOfferView.java | 21 ++-- .../offer/takeoffer/TakeOfferViewModel.java | 12 +++ .../editoffer/EditOfferDataModel.java | 2 +- .../editoffer/EditOfferViewModel.java | 4 + 16 files changed, 358 insertions(+), 94 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index bca9dc7cbf9..0764d33f078 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -182,9 +182,11 @@ private void placeOffer(Offer offer, double buyerSecurityDeposit, boolean useSavingsWallet, Consumer resultHandler) { + // TODO add support for triggerPrice parameter. If value is 0 it is interpreted as not used. Its an optional value openOfferManager.placeOffer(offer, buyerSecurityDeposit, useSavingsWallet, + 0, resultHandler::accept, log::error); diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 64740b38db3..61ae2f2ad00 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -72,11 +72,15 @@ public enum State { // Added in v1.5.3. // If market price reaches that trigger price the offer gets deactivated @Getter - @Setter - private long triggerPrice; + private final long triggerPrice; public OpenOffer(Offer offer) { + this(offer, 0); + } + + public OpenOffer(Offer offer, long triggerPrice) { this.offer = offer; + this.triggerPrice = triggerPrice; state = State.AVAILABLE; } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 496a0ac2f2a..252724305f2 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -358,6 +358,7 @@ public void onAwakeFromStandby() { public void placeOffer(Offer offer, double buyerSecurityDeposit, boolean useSavingsWallet, + long triggerPrice, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(offer.getMakerFee(), "makerFee must not be null"); @@ -382,7 +383,7 @@ public void placeOffer(Offer offer, PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol( model, transaction -> { - OpenOffer openOffer = new OpenOffer(offer); + OpenOffer openOffer = new OpenOffer(offer, triggerPrice); openOffers.add(openOffer); requestPersistence(); resultHandler.handleResult(transaction); @@ -486,6 +487,7 @@ public void editOpenOfferStart(OpenOffer openOffer, } public void editOpenOfferPublish(Offer editedOffer, + long triggerPrice, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -498,7 +500,7 @@ public void editOpenOfferPublish(Offer editedOffer, openOffer.setState(OpenOffer.State.CANCELED); openOffers.remove(openOffer); - OpenOffer editedOpenOffer = new OpenOffer(editedOffer); + OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice); editedOpenOffer.setState(originalState); openOffers.add(editedOpenOffer); @@ -855,7 +857,7 @@ private void maybeUpdatePersistedOffers() { updatedOffer.setPriceFeedService(priceFeedService); updatedOffer.setState(originalOfferState); - OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer); + OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); updatedOpenOffer.setState(originalOpenOfferState); openOffers.add(updatedOpenOffer); requestPersistence(); diff --git a/core/src/main/java/bisq/core/util/FormattingUtils.java b/core/src/main/java/bisq/core/util/FormattingUtils.java index c567bf1cefe..70229fb75ad 100644 --- a/core/src/main/java/bisq/core/util/FormattingUtils.java +++ b/core/src/main/java/bisq/core/util/FormattingUtils.java @@ -171,7 +171,7 @@ public static String formatMarketPrice(double price, String currencyCode) { return formatMarketPrice(price, 8); } - private static String formatMarketPrice(double price, int precision) { + public static String formatMarketPrice(double price, int precision) { return formatRoundedDoubleWithPrecision(price, precision); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 36b6f1002c2..0ffd76a9df9 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -105,7 +105,6 @@ shared.selectTradingAccount=Select trading account shared.fundFromSavingsWalletButton=Transfer funds from Bisq wallet shared.fundFromExternalWalletButton=Open your external wallet for funding shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed? -shared.distanceInPercent=Distance in % from market price shared.belowInPercent=Below % from market price shared.aboveInPercent=Above % from market price shared.enterPercentageValue=Enter % value @@ -455,6 +454,13 @@ createOffer.warning.buyAboveMarketPrice=You will always pay {0}% more than the c createOffer.tradeFee.descriptionBTCOnly=Trade fee createOffer.tradeFee.descriptionBSQEnabled=Select trade fee currency +createOffer.triggerPrice.prompt=Set optional trigger price +createOffer.triggerPrice.label=Deactivate offer if market price is {0} +createOffer.triggerPrice.tooltip=As protecting against drastic price movements you can set a trigger price which \ + deactivates the offer if the market price reaches that value. +createOffer.triggerPrice.invalid.tooLow=Trigger price must be higher than {0} +createOffer.triggerPrice.invalid.tooHigh=Trigger price must be lower than {0} + # new entries createOffer.placeOfferButton=Review: Place offer to {0} bitcoin createOffer.createOfferFundWalletInfo.headline=Fund your offer diff --git a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java index 1de6b84ab10..a63e0086b19 100644 --- a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java +++ b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java @@ -17,7 +17,12 @@ package bisq.desktop.main; +import bisq.desktop.util.validation.AltcoinValidator; +import bisq.desktop.util.validation.FiatPriceValidator; +import bisq.desktop.util.validation.MonetaryValidator; + import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.Offer; @@ -27,6 +32,9 @@ import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; import bisq.core.util.AveragePriceUtil; +import bisq.core.util.FormattingUtils; +import bisq.core.util.ParsingUtils; +import bisq.core.util.validation.InputValidator; import bisq.common.util.MathUtils; @@ -59,6 +67,45 @@ public PriceUtil(PriceFeedService priceFeedService, this.preferences = preferences; } + public static MonetaryValidator getPriceValidator(boolean isFiatCurrency) { + return isFiatCurrency ? + new FiatPriceValidator() : + new AltcoinValidator(); + } + + public static InputValidator.ValidationResult isTriggerPriceValid(String triggerPriceAsString, + Price price, + boolean isSellOffer, + boolean isFiatCurrency) { + if (triggerPriceAsString == null || triggerPriceAsString.isEmpty()) { + return new InputValidator.ValidationResult(true); + } + + InputValidator.ValidationResult result = getPriceValidator(isFiatCurrency).validate(triggerPriceAsString); + if (!result.isValid) { + return result; + } + + long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, price.getCurrencyCode()); + long priceAsLong = price.getValue(); + String priceAsString = FormattingUtils.formatPrice(price); + if ((isSellOffer && isFiatCurrency) || (!isSellOffer && !isFiatCurrency)) { + if (triggerPriceAsLong >= priceAsLong) { + return new InputValidator.ValidationResult(false, + Res.get("createOffer.triggerPrice.invalid.tooHigh", priceAsString)); + } else { + return new InputValidator.ValidationResult(true); + } + } else { + if (triggerPriceAsLong <= priceAsLong) { + return new InputValidator.ValidationResult(false, + Res.get("createOffer.triggerPrice.invalid.tooLow", priceAsString)); + } else { + return new InputValidator.ValidationResult(true); + } + } + } + public void recalculateBsq30DayAveragePrice() { bsq30DayAveragePrice = null; bsq30DayAveragePrice = getBsq30DayAveragePrice(); @@ -148,4 +195,39 @@ public Optional calculatePercentage(Offer offer, } return Optional.of(value); } + + public static long getMarketPriceAsLong(String inputValue, String currencyCode) { + if (inputValue == null || inputValue.isEmpty() || currencyCode == null) { + return 0; + } + + try { + int precision = getMarketPricePrecision(currencyCode); + String stringValue = reformatMarketPrice(inputValue, currencyCode); + return ParsingUtils.parsePriceStringToLong(currencyCode, stringValue, precision); + } catch (Throwable t) { + return 0; + } + } + + public static String reformatMarketPrice(String inputValue, String currencyCode) { + if (inputValue == null || inputValue.isEmpty() || currencyCode == null) { + return ""; + } + + double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue); + int precision = getMarketPricePrecision(currencyCode); + return FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision); + } + + public static String formatMarketPrice(long price, String currencyCode) { + int marketPricePrecision = getMarketPricePrecision(currencyCode); + double scaled = MathUtils.scaleDownByPowerOf10(price, marketPricePrecision); + return FormattingUtils.formatMarketPrice(scaled, marketPricePrecision); + } + + public static int getMarketPricePrecision(String currencyCode) { + return CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java index dca5c2ed38c..3c2f60fe3f8 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/notifications/MobileNotificationsView.java @@ -21,6 +21,7 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.InfoInputTextField; import bisq.desktop.components.InputTextField; +import bisq.desktop.main.PriceUtil; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.WebCamWindow; import bisq.desktop.util.FormBuilder; @@ -33,7 +34,6 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; -import bisq.core.monetary.Altcoin; import bisq.core.notifications.MobileMessage; import bisq.core.notifications.MobileNotificationService; import bisq.core.notifications.alerts.DisputeMsgEvents; @@ -693,6 +693,7 @@ private void fillPriceAlertFields() { currencyComboBox.getSelectionModel().select(optionalTradeCurrency.get()); onSelectedTradeCurrency(); + priceAlertHighInputTextField.setText(PriceUtil.formatMarketPrice(priceAlertFilter.getHigh(), currencyCode)); priceAlertHighInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getHigh() / 10000d, currencyCode)); priceAlertLowInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getLow() / 10000d, currencyCode)); } else { @@ -742,37 +743,13 @@ private InputValidator.ValidationResult isPriceInputValid(InputTextField inputTe } private long getPriceAsLong(InputTextField inputTextField) { - try { - String inputValue = inputTextField.getText(); - if (inputValue != null && !inputValue.isEmpty() && selectedPriceAlertTradeCurrency != null) { - double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue); - String currencyCode = selectedPriceAlertTradeCurrency; - int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? - Altcoin.SMALLEST_UNIT_EXPONENT : 2; - // We want to use the converted value not the inout value as we apply the converted value at focus out. - // E.g. if input is 5555.5555 it will be rounded to 5555.55 and we use that as the value for comparing - // low and high price... - String stringValue = FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision); - return ParsingUtils.parsePriceStringToLong(currencyCode, stringValue, precision); - } else { - return 0; - } - } catch (Throwable ignore) { - return 0; - } + return PriceUtil.getMarketPriceAsLong(inputTextField.getText(), selectedPriceAlertTradeCurrency); } private void applyPriceFormatting(InputTextField inputTextField) { try { - String inputValue = inputTextField.getText(); - if (inputValue != null && !inputValue.isEmpty() && selectedPriceAlertTradeCurrency != null) { - double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue); - String currencyCode = selectedPriceAlertTradeCurrency; - int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? - Altcoin.SMALLEST_UNIT_EXPONENT : 2; - String stringValue = FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision); - inputTextField.setText(stringValue); - } + String reformattedPrice = PriceUtil.reformatMarketPrice(inputTextField.getText(), selectedPriceAlertTradeCurrency); + inputTextField.setText(reformattedPrice); } catch (Throwable ignore) { updatePriceAlertFields(); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index e4fe866783f..de2897c4ce9 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -88,6 +88,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import lombok.Getter; + import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; @@ -129,6 +131,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs boolean isTabSelected; protected double marketPriceMargin = 0; private Coin txFeeFromFeeService = Coin.ZERO; + @Getter private boolean marketPriceAvailable; private int feeTxVsize = TxFeeEstimationService.TYPICAL_TX_WITH_1_INPUT_VSIZE; protected boolean allowAmountUpdate = true; @@ -137,6 +140,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs private final Predicate> isNonZeroAmount = (c) -> c.get() != null && !c.get().isZero(); private final Predicate> isNonZeroPrice = (p) -> p.get() != null && !p.get().isZero(); private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); + @Getter + private long triggerPrice; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -315,6 +321,7 @@ void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) { openOfferManager.placeOffer(offer, buyerSecurityDeposit.get(), useSavingsWallet, + triggerPrice, resultHandler, log::error); } @@ -467,6 +474,14 @@ OfferPayload.Direction getDirection() { return direction; } + boolean isSellOffer() { + return direction == OfferPayload.Direction.SELL; + } + + boolean isBuyOffer() { + return direction == OfferPayload.Direction.BUY; + } + AddressEntry getAddressEntry() { return addressEntry; } @@ -595,10 +610,6 @@ Coin getSecurityDeposit() { return isBuyOffer() ? getBuyerSecurityDepositAsCoin() : getSellerSecurityDepositAsCoin(); } - boolean isBuyOffer() { - return offerUtil.isBuyOffer(getDirection()); - } - public Coin getTxFee() { if (isCurrencyForMakerFeeBtc()) return txFeeFromFeeService; @@ -668,6 +679,18 @@ public ReadOnlyStringProperty getTradeCurrencyCode() { return tradeCurrencyCode; } + String getCurrencyCode() { + return tradeCurrencyCode.get(); + } + + boolean isCryptoCurrency() { + return CurrencyUtil.isCryptoCurrency(tradeCurrencyCode.get()); + } + + boolean isFiatCurrency() { + return CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get()); + } + ReadOnlyBooleanProperty getUseMarketBasedPrice() { return useMarketBasedPrice; } @@ -751,4 +774,8 @@ boolean canPlaceOffer() { public boolean isMinBuyerSecurityDeposit() { return !getBuyerSecurityDepositAsCoin().isGreaterThan(Restrictions.getMinBuyerSecurityDepositAsCoin()); } + + public void setTriggerPrice(long triggerPrice) { + this.triggerPrice = triggerPrice; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index 36d35cfdd70..21a58321075 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -68,6 +68,7 @@ import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; +import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import javafx.scene.Node; @@ -133,32 +134,31 @@ public abstract class MutableOfferView> exten private BusyAnimation waitingForFundsSpinner; private AutoTooltipButton nextButton, cancelButton1, cancelButton2, placeOfferButton; private Button priceTypeToggleButton; - private InputTextField fixedPriceTextField, marketBasedPriceTextField; + private InputTextField fixedPriceTextField, marketBasedPriceTextField, triggerPriceInputTextField; protected InputTextField amountTextField, minAmountTextField, volumeTextField, buyerSecurityDepositInputTextField; private TextField currencyTextField; private AddressTextField addressTextField; private BalanceTextField balanceTextField; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, - waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescription, tradeFeeDescriptionLabel, + waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, resultLabel, tradeFeeInBtcLabel, tradeFeeInBsqLabel, xLabel, fakeXLabel, buyerSecurityDepositLabel, - buyerSecurityDepositPercentageLabel; + buyerSecurityDepositPercentageLabel, triggerPriceCurrencyLabel, triggerPriceDescriptionLabel; protected Label amountBtcLabel, volumeCurrencyLabel, minAmountBtcLabel; private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; private ImageView qrCodeImageView; - private VBox currencySelection, fixedPriceBox, percentagePriceBox, - currencyTextFieldBox; + private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox, - minAmountValueCurrencyBox, advancedOptionsBox; + minAmountValueCurrencyBox, advancedOptionsBox, triggerPriceHBox; private Subscription isWaitingForFundsSubscription, balanceSubscription; private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, tradeFeeInBtcToggleListener, tradeFeeInBsqToggleListener, tradeFeeVisibleListener, - isMinBuyerSecurityDepositListener; + isMinBuyerSecurityDepositListener, triggerPriceFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, marketPriceMarginListener, volumeListener, buyerSecurityDepositInBTCListener; @@ -170,7 +170,7 @@ public abstract class MutableOfferView> exten private final List editOfferElements = new ArrayList<>(); private boolean clearXchangeWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated; private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField, - buyerSecurityDepositInfoInputTextField; + buyerSecurityDepositInfoInputTextField, triggerPriceInfoInputTextField; private AutoTooltipSlideToggleButton tradeFeeInBtcToggle, tradeFeeInBsqToggle; private Text xIcon, fakeXIcon; @@ -261,6 +261,9 @@ protected void doActivate() { tradeFeeInBsqToggle.setVisible(false); tradeFeeInBsqToggle.setManaged(false); } + + Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("createOffer.triggerPrice.tooltip")); + triggerPriceInfoInputTextField.setContentForPopOver(popOverLabel, AwesomeIcon.SHIELD); } } @@ -305,14 +308,11 @@ public void initWithData(OfferPayload.Direction direction, TradeCurrency tradeCu } if (direction == OfferPayload.Direction.BUY) { - placeOfferButton.setId("buy-button-big"); placeOfferButton.updateText(Res.get("createOffer.placeOfferButton", Res.get("shared.buy"))); - percentagePriceDescription.setText(Res.get("shared.belowInPercent")); } else { placeOfferButton.setId("sell-button-big"); placeOfferButton.updateText(Res.get("createOffer.placeOfferButton", Res.get("shared.sell"))); - percentagePriceDescription.setText(Res.get("shared.aboveInPercent")); } updatePriceToggle(); @@ -461,8 +461,12 @@ private void updateOfferElementsStyle() { priceValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); minAmountValueCurrencyBox.getStyleClass().remove(activeInputStyle); minAmountValueCurrencyBox.getStyleClass().add(readOnlyInputStyle); + triggerPriceHBox.getStyleClass().remove(activeInputStyle); + triggerPriceHBox.getStyleClass().add(readOnlyInputStyle); + GridPane.setColumnSpan(secondRowHBox, 1); priceTypeToggleButton.setVisible(false); + HBox.setMargin(priceTypeToggleButton, new Insets(16, -14, 0, 0)); resultLabel.getStyleClass().add("small"); xLabel.getStyleClass().add("small"); @@ -544,6 +548,10 @@ protected void close() { private void addBindings() { priceCurrencyLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getCounterCurrency(model.tradeCurrencyCode.get()), model.tradeCurrencyCode)); + triggerPriceCurrencyLabel.textProperty().bind(createStringBinding(() -> + CurrencyUtil.getCounterCurrency(model.tradeCurrencyCode.get()), model.tradeCurrencyCode)); + triggerPriceDescriptionLabel.textProperty().bind(model.triggerPriceDescription); + percentagePriceDescriptionLabel.textProperty().bind(model.percentagePriceDescription); marketBasedPriceLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty()); volumeCurrencyLabel.textProperty().bind(model.tradeCurrencyCode); priceDescriptionLabel.textProperty().bind(createStringBinding(() -> CurrencyUtil.getPriceWithCurrencyCode(model.tradeCurrencyCode.get(), "shared.fixedPriceInCurForCur"), model.tradeCurrencyCode)); @@ -551,6 +559,7 @@ private void addBindings() { amountTextField.textProperty().bindBidirectional(model.amount); minAmountTextField.textProperty().bindBidirectional(model.minAmount); fixedPriceTextField.textProperty().bindBidirectional(model.price); + triggerPriceInputTextField.textProperty().bindBidirectional(model.triggerPrice); marketBasedPriceTextField.textProperty().bindBidirectional(model.marketPriceMargin); volumeTextField.textProperty().bindBidirectional(model.volume); volumeTextField.promptTextProperty().bind(model.volumePromptLabel); @@ -569,6 +578,7 @@ private void addBindings() { amountTextField.validationResultProperty().bind(model.amountValidationResult); minAmountTextField.validationResultProperty().bind(model.minAmountValidationResult); fixedPriceTextField.validationResultProperty().bind(model.priceValidationResult); + triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); buyerSecurityDepositInputTextField.validationResultProperty().bind(model.buyerSecurityDepositValidationResult); @@ -591,12 +601,16 @@ private void addBindings() { private void removeBindings() { priceCurrencyLabel.textProperty().unbind(); + triggerPriceCurrencyLabel.textProperty().unbind(); + triggerPriceDescriptionLabel.textProperty().unbind(); + percentagePriceDescriptionLabel.textProperty().unbind(); volumeCurrencyLabel.textProperty().unbind(); priceDescriptionLabel.textProperty().unbind(); volumeDescriptionLabel.textProperty().unbind(); amountTextField.textProperty().unbindBidirectional(model.amount); minAmountTextField.textProperty().unbindBidirectional(model.minAmount); fixedPriceTextField.textProperty().unbindBidirectional(model.price); + triggerPriceInputTextField.textProperty().unbindBidirectional(model.triggerPrice); marketBasedPriceTextField.textProperty().unbindBidirectional(model.marketPriceMargin); marketBasedPriceLabel.prefWidthProperty().unbind(); volumeTextField.textProperty().unbindBidirectional(model.volume); @@ -616,6 +630,7 @@ private void removeBindings() { amountTextField.validationResultProperty().unbind(); minAmountTextField.validationResultProperty().unbind(); fixedPriceTextField.validationResultProperty().unbind(); + triggerPriceInputTextField.validationResultProperty().unbind(); volumeTextField.validationResultProperty().unbind(); buyerSecurityDepositInputTextField.validationResultProperty().unbind(); @@ -683,6 +698,11 @@ private void createListeners() { buyerSecurityDepositInputTextField.setText(model.buyerSecurityDeposit.get()); }; + triggerPriceFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutTriggerPriceTextField(oldValue, newValue); + triggerPriceInputTextField.setText(model.triggerPrice.get()); + }; + errorMessageListener = (o, oldValue, newValue) -> { if (newValue != null) UserThread.runAfter(() -> new Popup().error(Res.get("createOffer.amountPriceBox.error.message", model.errorMessage.get())) @@ -696,6 +716,7 @@ private void createListeners() { fixedPriceTextField.clear(); marketBasedPriceTextField.clear(); volumeTextField.clear(); + triggerPriceInputTextField.clear(); }; placeOfferCompletedListener = (o, oldValue, newValue) -> { @@ -740,7 +761,7 @@ private void createListeners() { buyerSecurityDepositInBTCListener = (observable, oldValue, newValue) -> { if (!newValue.equals("")) { - Label depositInBTCInfo = createPopoverLabel(model.getSecurityDepositPopOverLabel(newValue)); + Label depositInBTCInfo = OfferViewUtil.createPopOverLabel(model.getSecurityDepositPopOverLabel(newValue)); buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(depositInBTCInfo); } else { buyerSecurityDepositInfoInputTextField.setContentForInfoPopOver(null); @@ -749,7 +770,8 @@ private void createListeners() { volumeListener = (observable, oldValue, newValue) -> { if (!newValue.equals("") && CurrencyUtil.isFiatCurrency(model.tradeCurrencyCode.get())) { - volumeInfoInputTextField.setContentForPrivacyPopOver(createPopoverLabel(Res.get("offerbook.info.roundedFiatVolume"))); + Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("offerbook.info.roundedFiatVolume")); + volumeInfoInputTextField.setContentForPrivacyPopOver(popOverLabel); } else { volumeInfoInputTextField.hideIcon(); } @@ -777,7 +799,7 @@ private void createListeners() { } else { tooltip = Res.get("createOffer.info.buyAtMarketPrice"); } - final Label atMarketPriceLabel = createPopoverLabel(tooltip); + final Label atMarketPriceLabel = OfferViewUtil.createPopOverLabel(tooltip); marketBasedPriceInfoInputTextField.setContentForInfoPopOver(atMarketPriceLabel); } else if (newValue.contains("-")) { if (model.isSellOffer()) { @@ -785,7 +807,7 @@ private void createListeners() { } else { tooltip = Res.get("createOffer.warning.buyAboveMarketPrice", newValue.substring(1)); } - final Label negativePercentageLabel = createPopoverLabel(tooltip); + final Label negativePercentageLabel = OfferViewUtil.createPopOverLabel(tooltip); marketBasedPriceInfoInputTextField.setContentForWarningPopOver(negativePercentageLabel); } else if (!newValue.equals("")) { if (model.isSellOffer()) { @@ -793,7 +815,7 @@ private void createListeners() { } else { tooltip = Res.get("createOffer.info.buyBelowMarketPrice", newValue); } - Label positivePercentageLabel = createPopoverLabel(tooltip); + Label positivePercentageLabel = OfferViewUtil.createPopOverLabel(tooltip); marketBasedPriceInfoInputTextField.setContentForInfoPopOver(positivePercentageLabel); } } @@ -847,14 +869,6 @@ private void setIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { } } - private Label createPopoverLabel(String text) { - final Label label = new Label(text); - label.setPrefWidth(300); - label.setWrapText(true); - label.setPadding(new Insets(10)); - return label; - } - protected void updatePriceToggle() { int marketPriceAvailableValue = model.marketPriceAvailableProperty.get(); if (marketPriceAvailableValue > -1) { @@ -884,6 +898,7 @@ private void addListeners() { amountTextField.focusedProperty().addListener(amountFocusedListener); minAmountTextField.focusedProperty().addListener(minAmountFocusedListener); fixedPriceTextField.focusedProperty().addListener(priceFocusedListener); + triggerPriceInputTextField.focusedProperty().addListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); buyerSecurityDepositInputTextField.focusedProperty().addListener(buyerSecurityDepositFocusedListener); @@ -918,6 +933,7 @@ private void removeListeners() { amountTextField.focusedProperty().removeListener(amountFocusedListener); minAmountTextField.focusedProperty().removeListener(minAmountFocusedListener); fixedPriceTextField.focusedProperty().removeListener(priceFocusedListener); + triggerPriceInputTextField.focusedProperty().removeListener(triggerPriceFocusedListener); marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); buyerSecurityDepositInputTextField.focusedProperty().removeListener(buyerSecurityDepositFocusedListener); @@ -1291,10 +1307,10 @@ private void addAmountPriceFields() { marketBasedPriceLabel = priceAsPercentageTuple.third; editOfferElements.add(marketBasedPriceLabel); Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, - Res.get("shared.distanceInPercent")); - percentagePriceDescription = priceAsPercentageInputBoxTuple.first; + model.getPercentagePriceDescription()); + percentagePriceDescriptionLabel = priceAsPercentageInputBoxTuple.first; - getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, percentagePriceDescription, "small-icon-label"); + getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, percentagePriceDescriptionLabel, "small-icon-label"); percentagePriceBox = priceAsPercentageInputBoxTuple.second; @@ -1353,6 +1369,9 @@ private void updatePriceToggleButtons(boolean fixedPriceSelected) { if (!secondRowHBox.getChildren().contains(fixedPriceBox)) secondRowHBox.getChildren().add(2, fixedPriceBox); } + + triggerPriceVBox.setVisible(!fixedPriceSelected); + model.onFixPriceToggleChange(fixedPriceSelected); } private void addSecondRow() { @@ -1392,14 +1411,28 @@ private void addSecondRow() { // Fixed/Percentage toggle priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); editOfferElements.add(priceTypeToggleButton); - HBox.setMargin(priceTypeToggleButton, new Insets(16, 0, 0, 0)); + HBox.setMargin(priceTypeToggleButton, new Insets(16, 1.5, 0, 0)); priceTypeToggleButton.setOnAction((actionEvent) -> updatePriceToggleButtons(model.getDataModel().getUseMarketBasedPrice().getValue())); + // triggerPrice + Tuple3 triggerPriceTuple3 = getEditableValueBoxWithInfo(Res.get("createOffer.triggerPrice.prompt")); + triggerPriceHBox = triggerPriceTuple3.first; + triggerPriceInfoInputTextField = triggerPriceTuple3.second; + editOfferElements.add(triggerPriceInfoInputTextField); + triggerPriceInputTextField = triggerPriceInfoInputTextField.getInputTextField(); + triggerPriceCurrencyLabel = triggerPriceTuple3.third; + editOfferElements.add(triggerPriceCurrencyLabel); + Tuple2 triggerPriceTuple2 = getTradeInputBox(triggerPriceHBox, model.getTriggerPriceDescriptionLabel()); + triggerPriceDescriptionLabel = triggerPriceTuple2.first; + triggerPriceDescriptionLabel.setPrefWidth(290); + triggerPriceVBox = triggerPriceTuple2.second; + secondRowHBox = new HBox(); secondRowHBox.setSpacing(5); secondRowHBox.setAlignment(Pos.CENTER_LEFT); - secondRowHBox.getChildren().addAll(amountInputBoxTuple.second, fakeXLabel, fixedPriceBox, priceTypeToggleButton); + secondRowHBox.getChildren().addAll(amountInputBoxTuple.second, fakeXLabel, fixedPriceBox, priceTypeToggleButton, triggerPriceVBox); + GridPane.setColumnSpan(secondRowHBox, 2); GridPane.setRowIndex(secondRowHBox, ++gridRow); GridPane.setColumnIndex(secondRowHBox, 0); GridPane.setMargin(secondRowHBox, new Insets(0, 10, 10, 0)); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java index 0b0566988b1..f82ebe7844e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -20,6 +20,7 @@ import bisq.desktop.Navigation; import bisq.desktop.common.model.ActivatableWithDataModel; import bisq.desktop.main.MainView; +import bisq.desktop.main.PriceUtil; import bisq.desktop.main.funds.FundsView; import bisq.desktop.main.funds.deposit.DepositView; import bisq.desktop.main.overlays.popups.Popup; @@ -105,8 +106,8 @@ public abstract class MutableOfferViewModel ext protected final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; private final FiatVolumeValidator fiatVolumeValidator; - private final FiatPriceValidator fiatPriceValidator; - private final AltcoinValidator altcoinValidator; + private final FiatPriceValidator fiatPriceValidator, fiatTriggerPriceValidator; + private final AltcoinValidator altcoinValidator, altcoinTriggerPriceValidator; protected final OfferUtil offerUtil; private String amountDescription; @@ -124,6 +125,7 @@ public abstract class MutableOfferViewModel ext // The domain (dataModel) uses always the same price model (otherCurrencyBTC) // If we would change the price representation in the domain we would not be backward compatible public final StringProperty price = new SimpleStringProperty(); + public final StringProperty triggerPrice = new SimpleStringProperty(""); final StringProperty tradeFee = new SimpleStringProperty(); final StringProperty tradeFeeInBtcWithFiat = new SimpleStringProperty(); final StringProperty tradeFeeInBsqWithFiat = new SimpleStringProperty(); @@ -143,6 +145,8 @@ public abstract class MutableOfferViewModel ext final StringProperty errorMessage = new SimpleStringProperty(); final StringProperty tradeCurrencyCode = new SimpleStringProperty(); final StringProperty waitingForFundsText = new SimpleStringProperty(""); + final StringProperty triggerPriceDescription = new SimpleStringProperty(""); + final StringProperty percentagePriceDescription = new SimpleStringProperty(""); final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty cancelButtonDisabled = new SimpleBooleanProperty(); @@ -156,6 +160,7 @@ public abstract class MutableOfferViewModel ext final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); @@ -218,6 +223,9 @@ public MutableOfferViewModel(M dataModel, this.bsqFormatter = bsqFormatter; this.offerUtil = offerUtil; + fiatTriggerPriceValidator = new FiatPriceValidator(); + altcoinTriggerPriceValidator = new AltcoinValidator(); + paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); if (dataModel.getAddressEntry() != null) { @@ -277,12 +285,15 @@ private void addBindings() { totalToPay.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), dataModel.totalToPayAsCoinProperty())); - tradeAmount.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.getAmount().get()), dataModel.getAmount())); - tradeCurrencyCode.bind(dataModel.getTradeCurrencyCode()); + + triggerPriceDescription.bind(createStringBinding(this::getTriggerPriceDescriptionLabel, + dataModel.getTradeCurrencyCode())); + percentagePriceDescription.bind(createStringBinding(this::getPercentagePriceDescription, + dataModel.getTradeCurrencyCode())); } private void removeBindings() { @@ -291,6 +302,8 @@ private void removeBindings() { tradeCurrencyCode.unbind(); volumeDescriptionLabel.unbind(); volumePromptLabel.unbind(); + triggerPriceDescription.unbind(); + percentagePriceDescription.unbind(); } private void createListeners() { @@ -769,12 +782,44 @@ public void onFocusOutMinAmountTextField(boolean oldValue, boolean newValue) { } } + void onFocusOutTriggerPriceTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + onTriggerPriceTextFieldChanged(); + } + } + + void onTriggerPriceTextFieldChanged() { + String triggerPriceAsString = triggerPrice.get(); + InputValidator.ValidationResult result = PriceUtil.isTriggerPriceValid(triggerPriceAsString, + dataModel.getPrice().get(), + dataModel.isSellOffer(), + dataModel.isFiatCurrency()); + triggerPriceValidationResult.set(result); + updateButtonDisableState(); + if (result.isValid) { + // In case of 0 or empty string we set the string to empty string and data value to 0 + long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, dataModel.getCurrencyCode()); + dataModel.setTriggerPrice(triggerPriceAsLong); + if (dataModel.getTriggerPrice() == 0) { + triggerPrice.set(""); + } else { + triggerPrice.set(PriceUtil.formatMarketPrice(dataModel.getTriggerPrice(), dataModel.getCurrencyCode())); + } + } + } + + void onFixPriceToggleChange(boolean fixedPriceSelected) { + updateButtonDisableState(); + if (!fixedPriceSelected) { + onTriggerPriceTextFieldChanged(); + } + } + void onFocusOutPriceTextField(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isPriceInputValid(price.get()); - boolean isValid = result.isValid; priceValidationResult.set(result); - if (isValid) { + if (result.isValid) { setPriceToModel(); ignorePriceStringListener = true; if (dataModel.getPrice().get() != null) @@ -808,8 +853,11 @@ public void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newVa marketPriceMargin.set(FormattingUtils.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMargin() * 100, 2)); } - // We want to trigger a recalculation of the volume - UserThread.execute(() -> onFocusOutVolumeTextField(true, false)); + // We want to trigger a recalculation of the volume, as well as update trigger price validation + UserThread.execute(() -> { + onFocusOutVolumeTextField(true, false); + onTriggerPriceTextFieldChanged(); + }); } void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { @@ -1052,6 +1100,33 @@ public M getDataModel() { return dataModel; } + String getTriggerPriceDescriptionLabel() { + String details; + if (dataModel.isBuyOffer()) { + details = dataModel.isCryptoCurrency() ? + Res.get("account.notifications.marketAlert.message.msg.below") : + Res.get("account.notifications.marketAlert.message.msg.above"); + } else { + details = dataModel.isCryptoCurrency() ? + Res.get("account.notifications.marketAlert.message.msg.above") : + Res.get("account.notifications.marketAlert.message.msg.below"); + } + return Res.get("createOffer.triggerPrice.label", details); + } + + String getPercentagePriceDescription() { + if (dataModel.isBuyOffer()) { + return dataModel.isCryptoCurrency() ? + Res.get("shared.aboveInPercent") : + Res.get("shared.belowInPercent"); + } else { + return dataModel.isCryptoCurrency() ? + Res.get("shared.belowInPercent") : + Res.get("shared.aboveInPercent"); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// @@ -1195,8 +1270,7 @@ private void updateBuyerSecurityDeposit() { } } - private void updateButtonDisableState() { - log.debug("updateButtonDisableState"); + void updateButtonDisableState() { boolean inputDataValid = isBtcInputValid(amount.get()).isValid && isBtcInputValid(minAmount.get()).isValid && isPriceInputValid(price.get()).isValid && @@ -1206,6 +1280,10 @@ private void updateButtonDisableState() { isVolumeInputValid(DisplayUtils.formatVolume(dataModel.getMinVolume().get())).isValid && dataModel.isMinAmountLessOrEqualAmount(); + if (dataModel.useMarketBasedPrice.get() && dataModel.isMarketPriceAvailable()) { + inputDataValid = inputDataValid && triggerPriceValidationResult.get().isValid; + } + // validating the percentage deposit value only makes sense if it is actually used if (!dataModel.isMinBuyerSecurityDeposit()) { inputDataValid = inputDataValid && securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java new file mode 100644 index 00000000000..dfd6a375803 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java @@ -0,0 +1,36 @@ +/* + * 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.desktop.main.offer; + +import javafx.scene.control.Label; + +import javafx.geometry.Insets; + +/** + * Reusable methods for CreateOfferView, TakeOfferView or other related views + */ +public class OfferViewUtil { + public static Label createPopOverLabel(String text) { + final Label label = new Label(text); + label.setPrefWidth(300); + label.setWrapText(true); + label.setLineSpacing(1); + label.setPadding(new Insets(10)); + return label; + } +} 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 7e894924816..dc90ce04559 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 @@ -500,10 +500,18 @@ void calculateTotalToPay() { } } - private boolean isBuyOffer() { + boolean isBuyOffer() { return getDirection() == OfferPayload.Direction.BUY; } + boolean isSellOffer() { + return getDirection() == OfferPayload.Direction.SELL; + } + + boolean isCryptoCurrency() { + return CurrencyUtil.isCryptoCurrency(getCurrencyCode()); + } + @Nullable Coin getTakerFee(boolean isCurrencyForTakerFeeBtc) { Coin amount = this.amount.get(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java index 8309ed80662..0539962f546 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java @@ -37,6 +37,7 @@ import bisq.desktop.main.funds.FundsView; import bisq.desktop.main.funds.withdrawal.WithdrawalView; import bisq.desktop.main.offer.OfferView; +import bisq.desktop.main.offer.OfferViewUtil; import bisq.desktop.main.overlays.notifications.Notification; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; @@ -347,13 +348,12 @@ public void initWithData(Offer offer) { takeOfferButton.setId("buy-button-big"); takeOfferButton.updateText(Res.get("takeOffer.takeOfferButton", Res.get("shared.buy"))); nextButton.setId("buy-button"); - priceAsPercentageDescription.setText(Res.get("shared.aboveInPercent")); } else { takeOfferButton.setId("sell-button-big"); nextButton.setId("sell-button"); takeOfferButton.updateText(Res.get("takeOffer.takeOfferButton", Res.get("shared.sell"))); - priceAsPercentageDescription.setText(Res.get("shared.belowInPercent")); } + priceAsPercentageDescription.setText(model.getPercentagePriceDescription()); boolean showComboBox = model.getPossiblePaymentAccounts().size() > 1; paymentAccountsComboBox.setVisible(showComboBox); @@ -383,8 +383,10 @@ public void initWithData(Offer offer) { addressTextField.setPaymentLabel(model.getPaymentLabel()); addressTextField.setAddress(model.dataModel.getAddressEntry().getAddressString()); - if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) - volumeInfoTextField.setContentForPrivacyPopOver(createPopoverLabel(Res.get("offerbook.info.roundedFiatVolume"))); + if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) { + Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("offerbook.info.roundedFiatVolume")); + volumeInfoTextField.setContentForPrivacyPopOver(popOverLabel); + } if (offer.getPrice() == null) new Popup().warning(Res.get("takeOffer.noPriceFeedAvailable")) @@ -1121,8 +1123,7 @@ private void addSecondRow() { priceAsPercentageTextField = priceAsPercentageTuple.second; priceAsPercentageLabel = priceAsPercentageTuple.third; - Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, - Res.get("shared.distanceInPercent")); + Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, ""); priceAsPercentageDescription = priceAsPercentageInputBoxTuple.first; getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, priceAsPercentageDescription, "small-icon-label"); @@ -1273,14 +1274,6 @@ private GridPane createInfoPopover() { return infoGridPane; } - private Label createPopoverLabel(String text) { - final Label label = new Label(text); - label.setPrefWidth(300); - label.setWrapText(true); - label.setPadding(new Insets(10)); - return label; - } - private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { Label label = new AutoTooltipLabel(labelText); TextField textField = new TextField(value); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index e7f7b3b6cc8..717ef1ce47a 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -753,4 +753,16 @@ public Callback, ListCell> getPaymentAc ComboBox paymentAccountsComboBox) { return GUIUtil.getPaymentAccountListCellFactory(paymentAccountsComboBox, accountAgeWitnessService); } + + String getPercentagePriceDescription() { + if (dataModel.isBuyOffer()) { + return dataModel.isCryptoCurrency() ? + Res.get("shared.aboveInPercent") : + Res.get("shared.belowInPercent"); + } else { + return dataModel.isCryptoCurrency() ? + Res.get("shared.belowInPercent") : + Res.get("shared.aboveInPercent"); + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index 6524e311d1c..f72978fc0b6 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -227,7 +227,7 @@ public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler erro editedOffer.setPriceFeedService(priceFeedService); editedOffer.setState(Offer.State.AVAILABLE); - openOfferManager.editOpenOfferPublish(editedOffer, initialState, () -> { + openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { openOffer = null; resultHandler.handleResult(); }, errorMessageHandler); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java index 73b3c83781d..78a25290b6c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -18,6 +18,7 @@ package bisq.desktop.main.portfolio.editoffer; import bisq.desktop.Navigation; +import bisq.desktop.main.PriceUtil; import bisq.desktop.main.offer.MutableOfferViewModel; import bisq.desktop.util.validation.AltcoinValidator; import bisq.desktop.util.validation.BsqValidator; @@ -85,6 +86,9 @@ public void activate() { public void applyOpenOffer(OpenOffer openOffer) { dataModel.reset(); dataModel.applyOpenOffer(openOffer); + + dataModel.setTriggerPrice(openOffer.getTriggerPrice()); + triggerPrice.set(PriceUtil.formatMarketPrice(openOffer.getTriggerPrice(), openOffer.getOffer().getCurrencyCode())); } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { From f096351d60f85ad33150f7b49d99b21027d6ca0b Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 24 Dec 2020 21:46:14 -0500 Subject: [PATCH 09/16] Make rows smaller --- .../bisq/desktop/main/offer/MutableOfferDataModel.java | 2 +- .../main/portfolio/openoffer/OpenOffersView.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index de2897c4ce9..dc07ba38f6d 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -141,7 +141,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs private final Predicate> isNonZeroPrice = (p) -> p.get() != null && !p.get().isZero(); private final Predicate> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero(); @Getter - private long triggerPrice; + protected long triggerPrice; /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index f1ab5965d07..c6e8c99c4a8 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -79,7 +79,7 @@ import org.jetbrains.annotations.NotNull; -import static bisq.desktop.util.FormBuilder.getIconButton; +import static bisq.desktop.util.FormBuilder.getRegularIconButton; @FxmlView public class OpenOffersView extends ActivatableViewAndModel { @@ -635,6 +635,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null && !empty) { if (checkBox == null) { checkBox = new AutoTooltipSlideToggleButton(); + checkBox.setPadding(new Insets(-7, 0, -7, 0)); checkBox.setGraphic(iconView); } checkBox.setOnAction(event -> { @@ -659,6 +660,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }; } }); + deactivateItemColumn.setSortable(false); } private void setRemoveColumnCellFactory() { @@ -677,7 +679,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null && !empty) { if (button == null) { - button = getIconButton(MaterialDesignIcon.DELETE_FOREVER, "delete"); + button = getRegularIconButton(MaterialDesignIcon.DELETE_FOREVER, "delete"); button.setTooltip(new Tooltip(Res.get("shared.removeOffer"))); setGraphic(button); } @@ -693,6 +695,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }; } }); + removeItemColumn.setSortable(false); } private void setEditColumnCellFactory() { @@ -710,7 +713,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null && !empty) { if (button == null) { - button = getIconButton(MaterialDesignIcon.PENCIL); + button = getRegularIconButton(MaterialDesignIcon.PENCIL); button.setTooltip(new Tooltip(Res.get("shared.editOffer"))); setGraphic(button); } @@ -726,6 +729,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }; } }); + editItemColumn.setSortable(false); } public void setOpenOfferActionHandler(PortfolioView.OpenOfferActionHandler openOfferActionHandler) { From 39893a6aaf43be13bd3cfa465a991fde6c690709 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 24 Dec 2020 23:05:23 -0500 Subject: [PATCH 10/16] Add trigger price to OpenOffersView --- .../bisq/core/app/DomainInitialisation.java | 7 ++- .../bisq/core/offer/PriceEventHandler.java | 17 +++--- .../resources/i18n/displayStrings.properties | 1 + .../main/offer/MutableOfferDataModel.java | 2 +- .../main/offer/MutableOfferViewModel.java | 14 +++-- .../closedtrades/ClosedTradesView.java | 1 - .../editoffer/EditOfferDataModel.java | 5 +- .../editoffer/EditOfferViewModel.java | 13 +++- .../portfolio/openoffer/OpenOffersView.fxml | 1 + .../portfolio/openoffer/OpenOffersView.java | 59 +++++++++++++++---- .../openoffer/OpenOffersViewModel.java | 13 ++++ 11 files changed, 101 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index 754ba3f187d..303330651d9 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -34,6 +34,7 @@ import bisq.core.notifications.alerts.market.MarketAlerts; import bisq.core.notifications.alerts.price.PriceAlert; import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.PriceEventHandler; import bisq.core.payment.RevolutAccount; import bisq.core.payment.TradeLimits; import bisq.core.provider.fee.FeeService; @@ -106,6 +107,7 @@ public class DomainInitialisation { private final MarketAlerts marketAlerts; private final User user; private final DaoStateSnapshotService daoStateSnapshotService; + private final PriceEventHandler priceEventHandler; @Inject public DomainInitialisation(ClockWatcher clockWatcher, @@ -141,7 +143,8 @@ public DomainInitialisation(ClockWatcher clockWatcher, PriceAlert priceAlert, MarketAlerts marketAlerts, User user, - DaoStateSnapshotService daoStateSnapshotService) { + DaoStateSnapshotService daoStateSnapshotService, + PriceEventHandler priceEventHandler) { this.clockWatcher = clockWatcher; this.tradeLimits = tradeLimits; this.arbitrationManager = arbitrationManager; @@ -176,6 +179,7 @@ public DomainInitialisation(ClockWatcher clockWatcher, this.marketAlerts = marketAlerts; this.user = user; this.daoStateSnapshotService = daoStateSnapshotService; + this.priceEventHandler = priceEventHandler; } public void initDomainServices(Consumer rejectedTxErrorMessageHandler, @@ -254,6 +258,7 @@ public void initDomainServices(Consumer rejectedTxErrorMessageHandler, disputeMsgEvents.onAllServicesInitialized(); priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); + priceEventHandler.onAllServicesInitialized(); if (revolutAccountsUpdateHandler != null) { revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() diff --git a/core/src/main/java/bisq/core/offer/PriceEventHandler.java b/core/src/main/java/bisq/core/offer/PriceEventHandler.java index e006c7e6bdb..26c66de5d07 100644 --- a/core/src/main/java/bisq/core/offer/PriceEventHandler.java +++ b/core/src/main/java/bisq/core/offer/PriceEventHandler.java @@ -92,7 +92,8 @@ private void checkPriceThreshold(bisq.core.provider.price.MarketPrice marketPric } String currencyCode = openOffer.getOffer().getCurrencyCode(); - int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ? + boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); + int smallestUnitExponent = cryptoCurrency ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; long marketPriceAsLong = roundDoubleToLong( @@ -100,13 +101,15 @@ private void checkPriceThreshold(bisq.core.provider.price.MarketPrice marketPric long triggerPrice = openOffer.getTriggerPrice(); if (triggerPrice > 0) { OfferPayload.Direction direction = openOffer.getOffer().getDirection(); - boolean triggered = direction == OfferPayload.Direction.BUY ? - marketPriceAsLong > triggerPrice : - marketPriceAsLong < triggerPrice; + boolean isSellOffer = direction == OfferPayload.Direction.SELL; + boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency; + boolean triggered = condition ? + marketPriceAsLong < triggerPrice : + marketPriceAsLong > triggerPrice; if (triggered) { - log.error("Market price exceeded the trigger price of the open offer. " + - "We deactivate the open offer with ID {}. Currency: {}; offer direction: {}; " + - "Market price: {}; Upper price threshold : {}", + log.info("Market price exceeded the trigger price of the open offer.\n" + + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + + "Market price: {};\nTrigger price : {}", openOffer.getOffer().getShortId(), currencyCode, direction, diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 0ffd76a9df9..bce8de2a615 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -221,6 +221,7 @@ shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transac shared.numItemsLabel=Number of entries: {0} shared.filter=Filter shared.enabled=Enabled +shared.triggerPrice=Trigger price #################################################################### diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index dc07ba38f6d..39a8f81f7a1 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -679,7 +679,7 @@ public ReadOnlyStringProperty getTradeCurrencyCode() { return tradeCurrencyCode; } - String getCurrencyCode() { + public String getCurrencyCode() { return tradeCurrencyCode.get(); } 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 f82ebe7844e..bdbb89cb331 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -106,8 +106,8 @@ public abstract class MutableOfferViewModel ext protected final CoinFormatter btcFormatter; private final BsqFormatter bsqFormatter; private final FiatVolumeValidator fiatVolumeValidator; - private final FiatPriceValidator fiatPriceValidator, fiatTriggerPriceValidator; - private final AltcoinValidator altcoinValidator, altcoinTriggerPriceValidator; + private final FiatPriceValidator fiatPriceValidator; + private final AltcoinValidator altcoinValidator; protected final OfferUtil offerUtil; private String amountDescription; @@ -223,9 +223,6 @@ public MutableOfferViewModel(M dataModel, this.bsqFormatter = bsqFormatter; this.offerUtil = offerUtil; - fiatTriggerPriceValidator = new FiatPriceValidator(); - altcoinTriggerPriceValidator = new AltcoinValidator(); - paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); if (dataModel.getAddressEntry() != null) { @@ -788,8 +785,13 @@ void onFocusOutTriggerPriceTextField(boolean oldValue, boolean newValue) { } } - void onTriggerPriceTextFieldChanged() { + public void onTriggerPriceTextFieldChanged() { String triggerPriceAsString = triggerPrice.get(); + + // Error field does not update if there was an error and then another different error + // if not reset here. Not clear why... + triggerPriceValidationResult.set(new InputValidator.ValidationResult(true)); + InputValidator.ValidationResult result = PriceUtil.isTriggerPriceValid(triggerPriceAsString, dataModel.getPrice().get(), dataModel.isSellOffer(), diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 8f3039688f7..48762ccf56e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -313,7 +313,6 @@ private static > Comparator null } private void onWidthChange(double width) { - log.error("onWidthChange " + width); txFeeColumn.setVisible(width > 1200); tradeFeeColumn.setVisible(width > 1300); buyerSecurityDepositColumn.setVisible(width > 1400); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index f72978fc0b6..dc7d29c7c08 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -170,7 +170,10 @@ public void populateData() { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - if (offer.isUseMarketBasedPrice()) setMarketPriceMargin(offer.getMarketPriceMargin()); + setTriggerPrice(openOffer.getTriggerPrice()); + if (offer.isUseMarketBasedPrice()) { + setMarketPriceMargin(offer.getMarketPriceMargin()); + } } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java index 78a25290b6c..d4aab63268e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferViewModel.java @@ -80,15 +80,22 @@ public EditOfferViewModel(EditOfferDataModel dataModel, @Override public void activate() { super.activate(); + dataModel.populateData(); + + long triggerPriceAsLong = dataModel.getTriggerPrice(); + dataModel.setTriggerPrice(triggerPriceAsLong); + if (triggerPriceAsLong > 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); } public void applyOpenOffer(OpenOffer openOffer) { dataModel.reset(); dataModel.applyOpenOffer(openOffer); - - dataModel.setTriggerPrice(openOffer.getTriggerPrice()); - triggerPrice.set(PriceUtil.formatMarketPrice(openOffer.getTriggerPrice(), openOffer.getOffer().getCurrencyCode())); } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 192aef505da..96d38e8c820 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -47,6 +47,7 @@ + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index c6e8c99c4a8..6753becb4f1 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -89,7 +89,7 @@ public class OpenOffersView extends ActivatableViewAndModel priceColumn, deviationColumn, amountColumn, volumeColumn, marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, - removeItemColumn, editItemColumn, paymentMethodColumn; + removeItemColumn, editItemColumn, triggerPriceColumn, paymentMethodColumn; @FXML HBox searchBox; @FXML @@ -113,6 +113,7 @@ public class OpenOffersView extends ActivatableViewAndModel filteredList; private ChangeListener filterTextFieldListener; private PortfolioView.OpenOfferActionHandler openOfferActionHandler; + private ChangeListener widthListener; @Inject public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) { @@ -123,6 +124,7 @@ public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDet @Override public void initialize() { + widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); paymentMethodColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.paymentMethod"))); priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price"))); deviationColumn.setGraphic(new AutoTooltipTableColumn<>(Res.get("shared.deviation"), @@ -133,6 +135,7 @@ public void initialize() { directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType"))); dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); offerIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerId"))); + triggerPriceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.triggerPrice"))); deactivateItemColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.enabled"))); editItemColumn.setGraphic(new AutoTooltipLabel("")); removeItemColumn.setGraphic(new AutoTooltipLabel("")); @@ -148,6 +151,7 @@ public void initialize() { setDateColumnCellFactory(); setDeactivateColumnCellFactory(); setEditColumnCellFactory(); + setTriggerPriceColumnCellFactory(); setRemoveColumnCellFactory(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); @@ -159,6 +163,8 @@ public void initialize() { amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); priceColumn.setComparator(Comparator.comparing(o -> o.getOffer().getPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); deviationColumn.setComparator(Comparator.comparing(model::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder()))); + triggerPriceColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getTriggerPrice(), + Comparator.nullsFirst(Comparator.naturalOrder()))); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); @@ -245,6 +251,18 @@ protected void activate() { filterTextField.textProperty().addListener(filterTextFieldListener); applyFilteredListPredicate(filterTextField.getText()); + + root.widthProperty().addListener(widthListener); + onWidthChange(root.getWidth()); + } + + @Override + protected void deactivate() { + sortedList.comparatorProperty().unbind(); + exportButton.setOnAction(null); + + filterTextField.textProperty().removeListener(filterTextFieldListener); + root.widthProperty().removeListener(widthListener); } private void updateSelectToggleButtonState() { @@ -264,14 +282,6 @@ private void updateSelectToggleButtonState() { } } - @Override - protected void deactivate() { - sortedList.comparatorProperty().unbind(); - exportButton.setOnAction(null); - - filterTextField.textProperty().removeListener(filterTextFieldListener); - } - private void applyFilteredListPredicate(String filterString) { filteredList.setPredicate(item -> { if (filterString.isEmpty()) @@ -312,6 +322,10 @@ private void applyFilteredListPredicate(String filterString) { }); } + private void onWidthChange(double width) { + triggerPriceColumn.setVisible(width > 1200); + } + private void onDeactivateOpenOffer(OpenOffer openOffer) { if (model.isBootstrappedOrShowPopup()) { model.onDeactivateOpenOffer(openOffer, @@ -514,6 +528,30 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } + private void setTriggerPriceColumnCellFactory() { + triggerPriceColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + triggerPriceColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeAll("offer-disabled"); + if (item != null) { + if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); + setGraphic(new AutoTooltipLabel(model.getTriggerPrice(item))); + } else { + setGraphic(null); + } + } + }; + } + }); + } + private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( @@ -660,7 +698,6 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }; } }); - deactivateItemColumn.setSortable(false); } private void setRemoveColumnCellFactory() { @@ -695,7 +732,6 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }; } }); - removeItemColumn.setSortable(false); } private void setEditColumnCellFactory() { @@ -729,7 +765,6 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }; } }); - editItemColumn.setSortable(false); } public void setOpenOfferActionHandler(PortfolioView.OpenOfferActionHandler openOfferActionHandler) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 38dd5ee58cd..58c0391fab2 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -171,4 +171,17 @@ public String getMakerFeeAsString(OpenOffer openOffer) { btcFormatter.formatCoinWithCode(offer.getMakerFee()) : bsqFormatter.formatCoinWithCode(offer.getMakerFee()); } + + String getTriggerPrice(OpenOfferListItem item) { + if ((item == null)) + return ""; + Offer offer = item.getOffer(); + if (offer.isUseMarketBasedPrice()) { + return PriceUtil.formatMarketPrice(item.getOpenOffer().getTriggerPrice(), offer.getCurrencyCode()); + } else { + return Res.get("shared.na"); + } + } + + } From 64607de9246c7b7a6cade4bea143ca5ae33c1191 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Thu, 24 Dec 2020 23:44:27 -0500 Subject: [PATCH 11/16] Add trigger icon column --- .../bisq/core/app/DomainInitialisation.java | 10 +-- ...tHandler.java => TriggerPriceService.java} | 62 +++++++++++-------- .../resources/i18n/displayStrings.properties | 10 ++- .../openoffer/OpenOffersDataModel.java | 5 +- .../portfolio/openoffer/OpenOffersView.fxml | 7 ++- .../portfolio/openoffer/OpenOffersView.java | 59 +++++++++++++++--- 6 files changed, 109 insertions(+), 44 deletions(-) rename core/src/main/java/bisq/core/offer/{PriceEventHandler.java => TriggerPriceService.java} (71%) diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index 303330651d9..5843500dd6b 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -34,7 +34,7 @@ import bisq.core.notifications.alerts.market.MarketAlerts; import bisq.core.notifications.alerts.price.PriceAlert; import bisq.core.offer.OpenOfferManager; -import bisq.core.offer.PriceEventHandler; +import bisq.core.offer.TriggerPriceService; import bisq.core.payment.RevolutAccount; import bisq.core.payment.TradeLimits; import bisq.core.provider.fee.FeeService; @@ -107,7 +107,7 @@ public class DomainInitialisation { private final MarketAlerts marketAlerts; private final User user; private final DaoStateSnapshotService daoStateSnapshotService; - private final PriceEventHandler priceEventHandler; + private final TriggerPriceService triggerPriceService; @Inject public DomainInitialisation(ClockWatcher clockWatcher, @@ -144,7 +144,7 @@ public DomainInitialisation(ClockWatcher clockWatcher, MarketAlerts marketAlerts, User user, DaoStateSnapshotService daoStateSnapshotService, - PriceEventHandler priceEventHandler) { + TriggerPriceService triggerPriceService) { this.clockWatcher = clockWatcher; this.tradeLimits = tradeLimits; this.arbitrationManager = arbitrationManager; @@ -179,7 +179,7 @@ public DomainInitialisation(ClockWatcher clockWatcher, this.marketAlerts = marketAlerts; this.user = user; this.daoStateSnapshotService = daoStateSnapshotService; - this.priceEventHandler = priceEventHandler; + this.triggerPriceService = triggerPriceService; } public void initDomainServices(Consumer rejectedTxErrorMessageHandler, @@ -258,7 +258,7 @@ public void initDomainServices(Consumer rejectedTxErrorMessageHandler, disputeMsgEvents.onAllServicesInitialized(); priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); - priceEventHandler.onAllServicesInitialized(); + triggerPriceService.onAllServicesInitialized(); if (revolutAccountsUpdateHandler != null) { revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() diff --git a/core/src/main/java/bisq/core/offer/PriceEventHandler.java b/core/src/main/java/bisq/core/offer/TriggerPriceService.java similarity index 71% rename from core/src/main/java/bisq/core/offer/PriceEventHandler.java rename to core/src/main/java/bisq/core/offer/TriggerPriceService.java index 26c66de5d07..35caa6ce815 100644 --- a/core/src/main/java/bisq/core/offer/PriceEventHandler.java +++ b/core/src/main/java/bisq/core/offer/TriggerPriceService.java @@ -46,13 +46,13 @@ @Slf4j @Singleton -public class PriceEventHandler { +public class TriggerPriceService { private final OpenOfferManager openOfferManager; private final PriceFeedService priceFeedService; private final Map> openOffersByCurrency = new HashMap<>(); @Inject - public PriceEventHandler(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) { + public TriggerPriceService(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) { this.openOfferManager = openOfferManager; this.priceFeedService = priceFeedService; } @@ -85,10 +85,10 @@ private void onPriceFeedChanged() { }); } - private void checkPriceThreshold(bisq.core.provider.price.MarketPrice marketPrice, OpenOffer openOffer) { + public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) { Price price = openOffer.getOffer().getPrice(); if (price == null) { - return; + return false; } String currencyCode = openOffer.getOffer().getCurrencyCode(); @@ -99,27 +99,39 @@ private void checkPriceThreshold(bisq.core.provider.price.MarketPrice marketPric long marketPriceAsLong = roundDoubleToLong( scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent)); long triggerPrice = openOffer.getTriggerPrice(); - if (triggerPrice > 0) { - OfferPayload.Direction direction = openOffer.getOffer().getDirection(); - boolean isSellOffer = direction == OfferPayload.Direction.SELL; - boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency; - boolean triggered = condition ? - marketPriceAsLong < triggerPrice : - marketPriceAsLong > triggerPrice; - if (triggered) { - log.info("Market price exceeded the trigger price of the open offer.\n" + - "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + - "Market price: {};\nTrigger price : {}", - openOffer.getOffer().getShortId(), - currencyCode, - direction, - marketPrice.getPrice(), - MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent) - ); - openOfferManager.deactivateOpenOffer(openOffer, () -> { - }, errorMessage -> { - }); - } + if (triggerPrice <= 0) { + return false; + } + + OfferPayload.Direction direction = openOffer.getOffer().getDirection(); + boolean isSellOffer = direction == OfferPayload.Direction.SELL; + boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency; + return condition ? + marketPriceAsLong < triggerPrice : + marketPriceAsLong > triggerPrice; + } + + private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { + if (wasTriggered(marketPrice, openOffer)) { + String currencyCode = openOffer.getOffer().getCurrencyCode(); + int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + long triggerPrice = openOffer.getTriggerPrice(); + + log.info("Market price exceeded the trigger price of the open offer.\n" + + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + + "Market price: {};\nTrigger price : {}", + openOffer.getOffer().getShortId(), + currencyCode, + openOffer.getOffer().getDirection(), + marketPrice.getPrice(), + MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent) + ); + + openOfferManager.deactivateOpenOffer(openOffer, () -> { + }, errorMessage -> { + }); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index bce8de2a615..8b860f465d5 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -221,7 +221,6 @@ shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transac shared.numItemsLabel=Number of entries: {0} shared.filter=Filter shared.enabled=Enabled -shared.triggerPrice=Trigger price #################################################################### @@ -459,8 +458,8 @@ createOffer.triggerPrice.prompt=Set optional trigger price createOffer.triggerPrice.label=Deactivate offer if market price is {0} createOffer.triggerPrice.tooltip=As protecting against drastic price movements you can set a trigger price which \ deactivates the offer if the market price reaches that value. -createOffer.triggerPrice.invalid.tooLow=Trigger price must be higher than {0} -createOffer.triggerPrice.invalid.tooHigh=Trigger price must be lower than {0} +createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} +createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} # new entries createOffer.placeOfferButton=Review: Place offer to {0} bitcoin @@ -558,6 +557,11 @@ takeOffer.tac=With taking this offer I agree to the trade conditions as defined # Offerbook / Edit offer #################################################################### +openOffer.header.triggerPrice=Trigger price +openOffer.triggerPrice=Trigger price {0} +openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\ + Please edit the offer to define a new trigger price + editOffer.setPrice=Set price editOffer.confirmEdit=Confirm: Edit offer editOffer.publishOffer=Publishing your offer. diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersDataModel.java index 26421413a55..8d71374f7d4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersDataModel.java @@ -23,6 +23,7 @@ import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.TriggerPriceService; import bisq.core.provider.price.PriceFeedService; import bisq.common.handlers.ErrorMessageHandler; @@ -98,5 +99,7 @@ private void applyList() { list.sort((o1, o2) -> o2.getOffer().getDate().compareTo(o1.getOffer().getDate())); } - + boolean wasTriggered(OpenOffer openOffer) { + return TriggerPriceService.wasTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 96d38e8c820..ac16ef582b4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -47,14 +47,15 @@ - + - - + + + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 6753becb4f1..01ef58af3ad 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -89,7 +89,7 @@ public class OpenOffersView extends ActivatableViewAndModel priceColumn, deviationColumn, amountColumn, volumeColumn, marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, - removeItemColumn, editItemColumn, triggerPriceColumn, paymentMethodColumn; + removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn; @FXML HBox searchBox; @FXML @@ -135,7 +135,7 @@ public void initialize() { directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType"))); dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); offerIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerId"))); - triggerPriceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.triggerPrice"))); + triggerPriceColumn.setGraphic(new AutoTooltipLabel(Res.get("openOffer.header.triggerPrice"))); deactivateItemColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.enabled"))); editItemColumn.setGraphic(new AutoTooltipLabel("")); removeItemColumn.setGraphic(new AutoTooltipLabel("")); @@ -151,6 +151,7 @@ public void initialize() { setDateColumnCellFactory(); setDeactivateColumnCellFactory(); setEditColumnCellFactory(); + setTriggerIconColumnCellFactory(); setTriggerPriceColumnCellFactory(); setRemoveColumnCellFactory(); @@ -671,21 +672,23 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { + OpenOffer openOffer = item.getOpenOffer(); if (checkBox == null) { checkBox = new AutoTooltipSlideToggleButton(); checkBox.setPadding(new Insets(-7, 0, -7, 0)); checkBox.setGraphic(iconView); } + checkBox.setDisable(model.dataModel.wasTriggered(openOffer)); checkBox.setOnAction(event -> { - if (item.getOpenOffer().isDeactivated()) { - onActivateOpenOffer(item.getOpenOffer()); + if (openOffer.isDeactivated()) { + onActivateOpenOffer(openOffer); } else { - onDeactivateOpenOffer(item.getOpenOffer()); + onDeactivateOpenOffer(openOffer); } - updateState(item.getOpenOffer()); + updateState(openOffer); tableView.refresh(); }); - updateState(item.getOpenOffer()); + updateState(openOffer); setGraphic(checkBox); } else { setGraphic(null); @@ -734,6 +737,48 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } + private void setTriggerIconColumnCellFactory() { + triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + triggerIconColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.SHIELD_HALF_FULL); + boolean triggerPriceSet = item.getOpenOffer().getTriggerPrice() > 0; + button.setVisible(triggerPriceSet); + + if (model.dataModel.wasTriggered(item.getOpenOffer())) { + button.getGraphic().getStyleClass().add("warning"); + button.setTooltip(new Tooltip(Res.get("openOffer.triggered"))); + } else { + button.getGraphic().getStyleClass().remove("warning"); + button.setTooltip(new Tooltip(Res.get("openOffer.triggerPrice", model.getTriggerPrice(item)))); + } + setGraphic(button); + } + button.setOnAction(event -> onEditOpenOffer(item.getOpenOffer())); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + private void setEditColumnCellFactory() { editItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); editItemColumn.setCellFactory( From 2a86d5b5b59b0d6870cbd033ba4139d45bb45872 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 25 Dec 2020 00:13:55 -0500 Subject: [PATCH 12/16] Add triggerPrice to csv --- .../portfolio/openoffer/OpenOffersView.java | 17 +++++++++-------- .../openoffer/OpenOffersViewModel.java | 13 +++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 01ef58af3ad..873c73c0cae 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -210,8 +210,8 @@ protected void activate() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); - int reportColumns = tableColumns.size() - 2; // CSV report excludes the last columns (icons) - CSVEntryConverter headerConverter = transactionsListItem -> { + int reportColumns = tableColumns.size() - 3; // CSV report excludes the last columns (icons) + CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) { Node graphic = tableColumns.get(i).getGraphic(); @@ -234,11 +234,12 @@ protected void activate() { columns[2] = model.getMarketLabel(item); columns[3] = model.getPrice(item); columns[4] = model.getPriceDeviation(item); - columns[5] = model.getAmount(item); - columns[6] = model.getVolume(item); - columns[7] = model.getPaymentMethod(item); - columns[8] = model.getDirectionLabel(item); - columns[9] = String.valueOf(!item.getOpenOffer().isDeactivated()); + columns[5] = model.getTriggerPrice(item); + columns[6] = model.getAmount(item); + columns[7] = model.getVolume(item); + columns[8] = model.getPaymentMethod(item); + columns[9] = model.getDirectionLabel(item); + columns[10] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; @@ -340,7 +341,7 @@ private void onDeactivateOpenOffer(OpenOffer openOffer) { } private void onActivateOpenOffer(OpenOffer openOffer) { - if (model.isBootstrappedOrShowPopup()) { + if (model.isBootstrappedOrShowPopup() && !model.dataModel.wasTriggered(openOffer)) { model.onActivateOpenOffer(openOffer, () -> log.debug("Activate offer was successful"), (message) -> { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 58c0391fab2..e571e070036 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -173,15 +173,16 @@ public String getMakerFeeAsString(OpenOffer openOffer) { } String getTriggerPrice(OpenOfferListItem item) { - if ((item == null)) + if ((item == null)) { return ""; + } + Offer offer = item.getOffer(); - if (offer.isUseMarketBasedPrice()) { - return PriceUtil.formatMarketPrice(item.getOpenOffer().getTriggerPrice(), offer.getCurrencyCode()); - } else { + long triggerPrice = item.getOpenOffer().getTriggerPrice(); + if (!offer.isUseMarketBasedPrice() || triggerPrice <= 0) { return Res.get("shared.na"); + } else { + return PriceUtil.formatMarketPrice(triggerPrice, offer.getCurrencyCode()); } } - - } From 57c1ad28a52c8285319114277136f982d6769ee2 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 25 Dec 2020 00:14:21 -0500 Subject: [PATCH 13/16] Add dontShowAgain to edit offer success popup --- .../main/portfolio/editoffer/EditOfferView.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java index c78b180de60..c22e8b06b6c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java @@ -28,6 +28,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; +import bisq.core.user.DontShowAgainLookup; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.BsqFormatter; @@ -206,8 +207,13 @@ private void addConfirmEditGroup() { spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); //edit offer model.onPublishOffer(() -> { - log.debug("Edit offer was successful"); - new Popup().feedback(Res.get("editOffer.success")).show(); + String key = "editOfferSuccess"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .feedback(Res.get("editOffer.success")) + .dontShowAgainId(key) + .show(); + } spinnerInfoLabel.setText(""); busyAnimation.stop(); close(); From 4f96ade69d2d0b8c050ee9e992057afcf5a21df1 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 25 Dec 2020 00:14:47 -0500 Subject: [PATCH 14/16] Rename wrong params --- .../main/java/bisq/desktop/main/funds/locked/LockedView.java | 2 +- .../java/bisq/desktop/main/funds/reserved/ReservedView.java | 2 +- .../bisq/desktop/main/funds/transactions/TransactionsView.java | 2 +- .../java/bisq/desktop/main/market/trades/TradesChartsView.java | 2 +- .../desktop/main/portfolio/closedtrades/ClosedTradesView.java | 2 +- .../desktop/main/portfolio/failedtrades/FailedTradesView.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java index df3db4eadc0..0148bd4baf6 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java @@ -179,7 +179,7 @@ protected void activate() { exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); int reportColumns = tableColumns.size(); - CSVEntryConverter headerConverter = transactionsListItem -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java index cac62e6c449..f1135246655 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java @@ -179,7 +179,7 @@ protected void activate() { exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); int reportColumns = tableColumns.size(); - CSVEntryConverter headerConverter = transactionsListItem -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java index 3020828aa1f..4aefbe9b9d4 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java @@ -212,7 +212,7 @@ protected void activate() { exportButton.setOnAction(event -> { final ObservableList> tableColumns = tableView.getColumns(); final int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) - CSVEntryConverter headerConverter = transactionsListItem -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index b3d699cd80f..4320599881f 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -378,7 +378,7 @@ private void exportToCsv() { int reportColumns = tableColumns.size() + 1; boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get(); - CSVEntryConverter headerConverter = transactionsListItem -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; columns[0] = "Epoch time in ms"; for (int i = 0; i < tableColumns.size(); i++) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 48762ccf56e..6c4ebcf0f91 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -254,7 +254,7 @@ protected void activate() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { final ObservableList> tableColumns = tableView.getColumns(); - CSVEntryConverter headerConverter = transactionsListItem -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[ColumnNames.values().length]; for (ColumnNames m : ColumnNames.values()) { columns[m.ordinal()] = m.toString(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java index 85e11f51904..6297584d7f4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -201,7 +201,7 @@ protected void activate() { exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon) - CSVEntryConverter headerConverter = transactionsListItem -> { + CSVEntryConverter headerConverter = item -> { String[] columns = new String[reportColumns]; for (int i = 0; i < columns.length; i++) columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText(); From 1336cbb56972c968b922fb4ebcf4f7d84e4e7e4c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Fri, 25 Dec 2020 01:27:29 -0500 Subject: [PATCH 15/16] Fix test --- .../offerbook/OfferBookViewModelTest.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) 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 05a83cdfccb..39044f8d526 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 @@ -17,6 +17,8 @@ package bisq.desktop.main.offer.offerbook; +import bisq.desktop.main.PriceUtil; + import bisq.core.locale.Country; import bisq.core.locale.CryptoCurrency; import bisq.core.locale.FiatCurrency; @@ -41,6 +43,7 @@ import bisq.core.payment.payload.SpecificBanksAccountPayload; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.ImmutableCoinFormatter; @@ -90,6 +93,13 @@ public void setUp() { Res.setBaseCurrencyName(usd.getName()); } + private PriceUtil getPriceUtil() { + PriceFeedService priceFeedService = mock(PriceFeedService.class); + TradeStatisticsManager tradeStatisticsManager = mock(TradeStatisticsManager.class); + when(tradeStatisticsManager.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet()); + return new PriceUtil(priceFeedService, tradeStatisticsManager, empty); + } + @Ignore("PaymentAccountPayload needs to be set (has been changed with PB changes)") public void testIsAnyPaymentAccountValidForOffer() { Collection paymentAccounts; @@ -229,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForAmount.intValue()); } @@ -243,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); model.activate(); assertEquals(6, model.maxPlacesForAmount.intValue()); @@ -261,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); model.activate(); assertEquals(15, model.maxPlacesForAmount.intValue()); @@ -280,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForVolume.intValue()); } @@ -294,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); model.activate(); assertEquals(5, model.maxPlacesForVolume.intValue()); @@ -312,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); model.activate(); assertEquals(9, model.maxPlacesForVolume.intValue()); @@ -331,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForPrice.intValue()); } @@ -345,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); model.activate(); assertEquals(7, model.maxPlacesForPrice.intValue()); @@ -363,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue()); } @@ -391,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); model.activate(); assertEquals(8, model.maxPlacesForMarketPriceMargin.intValue()); //" (1.97%)" @@ -412,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, null, coinFormatter, new BsqFormatter()); + null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter()); final OfferBookListItem item = make(btcBuyItem.but( with(useMarketBasedPrice, true), From 681fed603e0a91f783905d2ec46e52d175cf1b60 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 27 Dec 2020 22:50:56 -0500 Subject: [PATCH 16/16] Fix log --- core/src/main/java/bisq/core/offer/TriggerPriceService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/offer/TriggerPriceService.java b/core/src/main/java/bisq/core/offer/TriggerPriceService.java index 35caa6ce815..da82858f09a 100644 --- a/core/src/main/java/bisq/core/offer/TriggerPriceService.java +++ b/core/src/main/java/bisq/core/offer/TriggerPriceService.java @@ -121,7 +121,7 @@ private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) { log.info("Market price exceeded the trigger price of the open offer.\n" + "We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" + - "Market price: {};\nTrigger price : {}", + "Market price: {};\nTrigger price: {}", openOffer.getOffer().getShortId(), currencyCode, openOffer.getOffer().getDirection(),