Merge branch 'master' into 01-misc

This commit is contained in:
ghubstan 2020-12-28 14:38:39 -03:00
commit 25fbd3518e
No known key found for this signature in database
GPG key ID: E35592D6800A861E
40 changed files with 1110 additions and 340 deletions

View file

@ -182,9 +182,11 @@ class CoreOffersService {
double buyerSecurityDeposit,
boolean useSavingsWallet,
Consumer<Transaction> 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);

View file

@ -34,6 +34,7 @@ import bisq.core.notifications.alerts.TradeEvents;
import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOfferManager;
import bisq.core.offer.TriggerPriceService;
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 TriggerPriceService triggerPriceService;
@Inject
public DomainInitialisation(ClockWatcher clockWatcher,
@ -141,7 +143,8 @@ public class DomainInitialisation {
PriceAlert priceAlert,
MarketAlerts marketAlerts,
User user,
DaoStateSnapshotService daoStateSnapshotService) {
DaoStateSnapshotService daoStateSnapshotService,
TriggerPriceService triggerPriceService) {
this.clockWatcher = clockWatcher;
this.tradeLimits = tradeLimits;
this.arbitrationManager = arbitrationManager;
@ -176,6 +179,7 @@ public class DomainInitialisation {
this.marketAlerts = marketAlerts;
this.user = user;
this.daoStateSnapshotService = daoStateSnapshotService;
this.triggerPriceService = triggerPriceService;
}
public void initDomainServices(Consumer<String> rejectedTxErrorMessageHandler,
@ -254,6 +258,7 @@ public class DomainInitialisation {
disputeMsgEvents.onAllServicesInitialized();
priceAlert.onAllServicesInitialized();
marketAlerts.onAllServicesInitialized();
triggerPriceService.onAllServicesInitialized();
if (revolutAccountsUpdateHandler != null) {
revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()

View file

@ -69,9 +69,18 @@ public final class OpenOffer implements Tradable {
@Nullable
private NodeAddress refundAgentNodeAddress;
// Added in v1.5.3.
// If market price reaches that trigger price the offer gets deactivated
@Getter
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;
}
@ -83,12 +92,14 @@ public final class OpenOffer implements Tradable {
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 +109,7 @@ public final class OpenOffer implements Tradable {
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 +124,8 @@ public final class OpenOffer implements Tradable {
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 +191,7 @@ public final class OpenOffer implements Tradable {
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n mediatorNodeAddress=" + mediatorNodeAddress +
",\n refundAgentNodeAddress=" + refundAgentNodeAddress +
",\n triggerPrice=" + triggerPrice +
"\n}";
}
}

View file

@ -358,6 +358,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
public void editOpenOfferPublish(Offer editedOffer,
long triggerPrice,
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
@ -498,7 +500,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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();

View file

@ -0,0 +1,163 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.offer;
import bisq.core.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 TriggerPriceService {
private final OpenOfferManager openOfferManager;
private final PriceFeedService priceFeedService;
private final Map<String, Set<OpenOffer>> openOffersByCurrency = new HashMap<>();
@Inject
public TriggerPriceService(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) {
this.openOfferManager = openOfferManager;
this.priceFeedService = priceFeedService;
}
public void onAllServicesInitialized() {
openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) 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));
});
}
public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
Price price = openOffer.getOffer().getPrice();
if (price == null) {
return false;
}
String currencyCode = openOffer.getOffer().getCurrencyCode();
boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
int smallestUnitExponent = cryptoCurrency ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long marketPriceAsLong = roundDoubleToLong(
scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent));
long triggerPrice = openOffer.getTriggerPrice();
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 -> {
});
}
}
private void onAddedOpenOffers(List<? extends OpenOffer> 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<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode();
if (openOffersByCurrency.containsKey(currencyCode)) {
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
set.remove(openOffer);
if (set.isEmpty()) {
openOffersByCurrency.remove(currencyCode);
}
}
});
}
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.agent;
import bisq.core.locale.Res;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class DisputeAgentLookupMap {
// See also: https://bisq.wiki/Finding_your_mediator
@Nullable
public static String getKeyBaseUserName(String fullAddress) {
switch (fullAddress) {
case "sjlho4zwp3gecspf.onion:9999":
return "leo816";
case "wizbisqzd7ku25di7p2ztsajioabihlnyp5lq5av66tmu7do2dke2tid.onion:9999":
return "wiz";
case "apbp7ubuyezav4hy.onion:9999":
return "bisq_knight";
case "a56olqlmmpxrn5q34itq5g5tb5d3fg7vxekpbceq7xqvfl3cieocgsyd.onion:9999":
return "leo816";
case "3z5jnirlccgxzoxc6zwkcgwj66bugvqplzf6z2iyd5oxifiaorhnanqd.onion:9999":
return "refundagent2";
default:
log.warn("No user name for dispute agent with address {} found.", fullAddress);
return Res.get("shared.na");
}
}
}

View file

@ -171,7 +171,7 @@ public class FormattingUtils {
return formatMarketPrice(price, 8);
}
private static String formatMarketPrice(double price, int precision) {
public static String formatMarketPrice(double price, int precision) {
return formatRoundedDoubleWithPrecision(price, precision);
}

View file

@ -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=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
createOffer.createOfferFundWalletInfo.headline=Fund your offer
@ -551,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.
@ -1114,6 +1125,7 @@ support.error=Receiver could not process message. Error: {0}
support.buyerAddress=BTC buyer address
support.sellerAddress=BTC seller address
support.role=Role
support.agent=Support agent
support.state=State
support.closed=Closed
support.open=Open

View file

@ -19,6 +19,7 @@ package bisq.desktop.components;
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 javafx.beans.property.StringProperty;
import lombok.Getter;
import static bisq.desktop.util.FormBuilder.getIcon;
import javax.annotation.Nullable;
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 class InfoInputTextField extends AnchorPane {
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();
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();
}
});
getChildren().addAll(inputTextField, infoIcon, warningIcon, privacyIcon);
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;
hideIcons();
setActionHandlers(node);
setContentForPopOver(node, AwesomeIcon.EYE_CLOSE);
}
public void hideInfoContent() {
currentIcon = null;
hideIcons();
public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon) {
setContentForPopOver(node, awesomeIcon, null);
}
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 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 class InfoInputTextField extends AnchorPane {
return text.get();
}
public final StringProperty textProperty() {
public StringProperty textProperty() {
return text;
}
@ -155,28 +139,18 @@ public class InfoInputTextField extends AnchorPane {
// 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;
}

View file

@ -0,0 +1,233 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.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;
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.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.validation.InputValidator;
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 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();
}
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<Double> 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<Double> 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);
}
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;
}
}

View file

@ -21,6 +21,7 @@ import bisq.desktop.common.view.ActivatableView;
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.desktop.util.validation.PercentageNumberValidator;
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 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
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 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
}
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();
}

View file

@ -179,7 +179,7 @@ public class LockedView extends ActivatableView<VBox, Void> {
exportButton.setOnAction(event -> {
ObservableList<TableColumn<LockedListItem, ?>> tableColumns = tableView.getColumns();
int reportColumns = tableColumns.size();
CSVEntryConverter<LockedListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<LockedListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View file

@ -179,7 +179,7 @@ public class ReservedView extends ActivatableView<VBox, Void> {
exportButton.setOnAction(event -> {
ObservableList<TableColumn<ReservedListItem, ?>> tableColumns = tableView.getColumns();
int reportColumns = tableColumns.size();
CSVEntryConverter<ReservedListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<ReservedListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View file

@ -212,7 +212,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
exportButton.setOnAction(event -> {
final ObservableList<TableColumn<TransactionsListItem, ?>> tableColumns = tableView.getColumns();
final int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon)
CSVEntryConverter<TransactionsListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<TransactionsListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View file

@ -378,7 +378,7 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
int reportColumns = tableColumns.size() + 1;
boolean showAllTradeCurrencies = model.showAllTradeCurrenciesProperty.get();
CSVEntryConverter<TradeStatistics3ListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<TradeStatistics3ListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
columns[0] = "Epoch time in ms";
for (int i = 0; i < tableColumns.size(); i++) {

View file

@ -88,6 +88,8 @@ import java.util.Optional;
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<ObjectProperty<Coin>> isNonZeroAmount = (c) -> c.get() != null && !c.get().isZero();
private final Predicate<ObjectProperty<Price>> isNonZeroPrice = (p) -> p.get() != null && !p.get().isZero();
private final Predicate<ObjectProperty<Volume>> isNonZeroVolume = (v) -> v.get() != null && !v.get().isZero();
@Getter
protected long triggerPrice;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
@ -315,6 +321,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
openOfferManager.placeOffer(offer,
buyerSecurityDeposit.get(),
useSavingsWallet,
triggerPrice,
resultHandler,
log::error);
}
@ -467,6 +474,14 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
return direction;
}
boolean isSellOffer() {
return direction == OfferPayload.Direction.SELL;
}
boolean isBuyOffer() {
return direction == OfferPayload.Direction.BUY;
}
AddressEntry getAddressEntry() {
return addressEntry;
}
@ -595,10 +610,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
return isBuyOffer() ? getBuyerSecurityDepositAsCoin() : getSellerSecurityDepositAsCoin();
}
boolean isBuyOffer() {
return offerUtil.isBuyOffer(getDirection());
}
public Coin getTxFee() {
if (isCurrencyForMakerFeeBtc())
return txFeeFromFeeService;
@ -668,6 +679,18 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
return tradeCurrencyCode;
}
public 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 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
public boolean isMinBuyerSecurityDeposit() {
return !getBuyerSecurityDepositAsCoin().isGreaterThan(Restrictions.getMinBuyerSecurityDepositAsCoin());
}
public void setTriggerPrice(long triggerPrice) {
this.triggerPrice = triggerPrice;
}
}

View file

@ -68,6 +68,7 @@ import org.bitcoinj.core.Coin;
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<M extends MutableOfferViewModel<?>> 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<PaymentAccount> paymentAccountsComboBox;
private ComboBox<TradeCurrency> 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<Boolean> amountFocusedListener, minAmountFocusedListener, volumeFocusedListener,
buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener,
priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener,
tradeFeeInBtcToggleListener, tradeFeeInBsqToggleListener, tradeFeeVisibleListener,
isMinBuyerSecurityDepositListener;
isMinBuyerSecurityDepositListener, triggerPriceFocusedListener;
private ChangeListener<Coin> missingCoinListener;
private ChangeListener<String> tradeCurrencyCodeListener, errorMessageListener,
marketPriceMarginListener, volumeListener, buyerSecurityDepositInBTCListener;
@ -170,10 +170,11 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
private final List<Node> editOfferElements = new ArrayList<>();
private boolean clearXchangeWarningDisplayed, fasterPaymentsWarningDisplayed, isActivated;
private InfoInputTextField marketBasedPriceInfoInputTextField, volumeInfoInputTextField,
buyerSecurityDepositInfoInputTextField;
buyerSecurityDepositInfoInputTextField, triggerPriceInfoInputTextField;
private AutoTooltipSlideToggleButton tradeFeeInBtcToggle, tradeFeeInBsqToggle;
private Text xIcon, fakeXIcon;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@ -237,7 +238,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
if (waitingForFundsSpinner != null)
waitingForFundsSpinner.play();
//directionLabel.setText(model.getDirectionLabel());
amountDescriptionLabel.setText(model.getAmountDescription());
addressTextField.setAddress(model.getAddressAsString());
addressTextField.setPaymentLabel(model.getPaymentLabel());
@ -261,6 +261,9 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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 abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
}
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();
@ -449,8 +449,8 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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);
@ -461,6 +461,12 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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");
@ -542,7 +548,10 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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));
@ -550,6 +559,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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);
@ -568,6 +578,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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);
@ -590,16 +601,16 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
private void removeBindings() {
priceCurrencyLabel.textProperty().unbind();
fixedPriceTextField.disableProperty().unbind();
priceCurrencyLabel.disableProperty().unbind();
marketBasedPriceTextField.disableProperty().unbind();
marketBasedPriceLabel.disableProperty().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);
@ -619,6 +630,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
amountTextField.validationResultProperty().unbind();
minAmountTextField.validationResultProperty().unbind();
fixedPriceTextField.validationResultProperty().unbind();
triggerPriceInputTextField.validationResultProperty().unbind();
volumeTextField.validationResultProperty().unbind();
buyerSecurityDepositInputTextField.validationResultProperty().unbind();
@ -686,6 +698,11 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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()))
@ -699,6 +716,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
fixedPriceTextField.clear();
marketBasedPriceTextField.clear();
volumeTextField.clear();
triggerPriceInputTextField.clear();
};
placeOfferCompletedListener = (o, oldValue, newValue) -> {
@ -743,7 +761,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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);
@ -752,9 +770,10 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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.hideInfoContent();
volumeInfoInputTextField.hideIcon();
}
};
@ -780,7 +799,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
} 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()) {
@ -788,7 +807,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
} 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()) {
@ -796,7 +815,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
} else {
tooltip = Res.get("createOffer.info.buyBelowMarketPrice", newValue);
}
Label positivePercentageLabel = createPopoverLabel(tooltip);
Label positivePercentageLabel = OfferViewUtil.createPopOverLabel(tooltip);
marketBasedPriceInfoInputTextField.setContentForInfoPopOver(positivePercentageLabel);
}
}
@ -850,14 +869,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
}
}
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) {
@ -887,6 +898,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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);
@ -921,6 +933,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
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);
@ -1294,10 +1307,10 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
marketBasedPriceLabel = priceAsPercentageTuple.third;
editOfferElements.add(marketBasedPriceLabel);
Tuple2<Label, VBox> 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;
@ -1356,6 +1369,9 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
if (!secondRowHBox.getChildren().contains(fixedPriceBox))
secondRowHBox.getChildren().add(2, fixedPriceBox);
}
triggerPriceVBox.setVisible(!fixedPriceSelected);
model.onFixPriceToggleChange(fixedPriceSelected);
}
private void addSecondRow() {
@ -1387,7 +1403,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(minAmountValueCurrencyBox, Res.get("createOffer.amountPriceBox.minAmountDescription"));
fakeXLabel = new Label();
fakeXIcon = getIconForLabel(MaterialDesignIcon.CLOSE, "2em", fakeXLabel);
fakeXLabel.getStyleClass().add("opaque-icon-character");
@ -1396,16 +1411,28 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
// 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()));
secondRowHBox = new HBox();
// triggerPrice
Tuple3<HBox, InfoInputTextField, Label> 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<Label, VBox> 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));

View file

@ -20,6 +20,7 @@ package bisq.desktop.main.offer;
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;
@ -124,6 +125,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> ext
final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> minAmountValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> priceValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true));
final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> buyerSecurityDepositValidationResult = new SimpleObjectProperty<>();
@ -277,12 +282,15 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
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 +299,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
tradeCurrencyCode.unbind();
volumeDescriptionLabel.unbind();
volumePromptLabel.unbind();
triggerPriceDescription.unbind();
percentagePriceDescription.unbind();
}
private void createListeners() {
@ -769,12 +779,49 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
}
void onFocusOutTriggerPriceTextField(boolean oldValue, boolean newValue) {
if (oldValue && !newValue) {
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(),
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 +855,11 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
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 +1102,33 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
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 +1272,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
}
private void updateButtonDisableState() {
log.debug("updateButtonDisableState");
void updateButtonDisableState() {
boolean inputDataValid = isBtcInputValid(amount.get()).isValid &&
isBtcInputValid(minAmount.get()).isValid &&
isPriceInputValid(price.get()).isValid &&
@ -1206,6 +1282,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
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;

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -20,6 +20,7 @@ package bisq.desktop.main.offer.offerbook;
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.CurrencyUtil;
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.offer.OpenOfferManager;
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.network.p2p.P2PService;
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 java.util.stream.Collectors;
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 @@ class OfferBookViewModel extends ActivatableViewModel {
FilterManager filterManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
TradeStatisticsManager tradeStatisticsManager,
PriceUtil priceUtil,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
super();
@ -175,7 +164,7 @@ class OfferBookViewModel extends ActivatableViewModel {
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 @@ class OfferBookViewModel extends ActivatableViewModel {
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 @@ class OfferBookViewModel extends ActivatableViewModel {
}
public Optional<Double> 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<Double> 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) {

View file

@ -500,10 +500,18 @@ class TakeOfferDataModel extends OfferDataModel {
}
}
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();

View file

@ -37,6 +37,7 @@ import bisq.desktop.main.dao.wallet.receive.BsqReceiveView;
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 class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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 class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
priceAsPercentageTextField = priceAsPercentageTuple.second;
priceAsPercentageLabel = priceAsPercentageTuple.third;
Tuple2<Label, VBox> priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox,
Res.get("shared.distanceInPercent"));
Tuple2<Label, VBox> priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, "");
priceAsPercentageDescription = priceAsPercentageInputBoxTuple.first;
getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, priceAsPercentageDescription, "small-icon-label");
@ -1273,14 +1274,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
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);

View file

@ -753,4 +753,16 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
ComboBox<PaymentAccount> 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");
}
}
}

View file

@ -33,6 +33,7 @@ import bisq.core.payment.payload.PaymentMethod;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.agent.DisputeAgentLookupMap;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.refund.RefundManager;
@ -193,17 +194,20 @@ public class ContractWindow extends Overlay<ContractWindow> {
sellerPaymentAccountPayload.getPaymentDetails()).second.setMouseTransparent(false);
String title = "";
String agentKeyBaseUserName = "";
if (dispute.getSupportType() != null) {
switch (dispute.getSupportType()) {
case ARBITRATION:
title = Res.get("shared.selectedArbitrator");
break;
case MEDIATION:
agentKeyBaseUserName = DisputeAgentLookupMap.getKeyBaseUserName(contract.getMediatorNodeAddress().getFullAddress());
title = Res.get("shared.selectedMediator");
break;
case TRADE:
break;
case REFUND:
agentKeyBaseUserName = DisputeAgentLookupMap.getKeyBaseUserName(contract.getRefundAgentNodeAddress().getFullAddress());
title = Res.get("shared.selectedRefundAgent");
break;
}
@ -212,7 +216,8 @@ public class ContractWindow extends Overlay<ContractWindow> {
if (disputeManager != null) {
NodeAddress agentNodeAddress = disputeManager.getAgentNodeAddress(dispute);
if (agentNodeAddress != null) {
addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, title, agentNodeAddress.getFullAddress());
String value = agentKeyBaseUserName + " (" + agentNodeAddress.getFullAddress() + ")";
addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, title, value);
}
}

View file

@ -32,6 +32,7 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.support.dispute.agent.DisputeAgentLookupMap;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
@ -346,6 +347,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
textArea.setText(trade.getContractAsJson());
String data = "Contract as json:\n";
data += trade.getContractAsJson();
data += "\n\nOther detail data:";
data += "\n\nBuyerMultiSigPubKeyHex: " + Utils.HEX.encode(contract.getBuyerMultiSigPubKey());
data += "\nSellerMultiSigPubKeyHex: " + Utils.HEX.encode(contract.getSellerMultiSigPubKey());
if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode())) {
@ -358,6 +360,9 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
data += "\n\nRaw deposit transaction as hex:\n" + depositTxAsHex;
}
data += "\n\nSelected mediator: " + DisputeAgentLookupMap.getKeyBaseUserName(contract.getMediatorNodeAddress().getFullAddress());
data += "\nSelected arbitrator (refund agent): " + DisputeAgentLookupMap.getKeyBaseUserName(contract.getRefundAgentNodeAddress().getFullAddress());
textArea.setText(data);
textArea.setPrefHeight(50);
textArea.setEditable(false);

View file

@ -254,7 +254,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
exportButton.setOnAction(event -> {
final ObservableList<TableColumn<ClosedTradableListItem, ?>> tableColumns = tableView.getColumns();
CSVEntryConverter<ClosedTradableListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<ClosedTradableListItem> headerConverter = item -> {
String[] columns = new String[ColumnNames.values().length];
for (ColumnNames m : ColumnNames.values()) {
columns[m.ordinal()] = m.toString();

View file

@ -170,7 +170,10 @@ class EditOfferDataModel extends MutableOfferDataModel {
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) {
@ -227,7 +230,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
editedOffer.setPriceFeedService(priceFeedService);
editedOffer.setState(Offer.State.AVAILABLE);
openOfferManager.editOpenOfferPublish(editedOffer, initialState, () -> {
openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> {
openOffer = null;
resultHandler.handleResult();
}, errorMessageHandler);

View file

@ -28,6 +28,7 @@ import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
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 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
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();

View file

@ -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;
@ -79,7 +80,17 @@ class EditOfferViewModel extends MutableOfferViewModel<EditOfferDataModel> {
@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) {

View file

@ -201,7 +201,7 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
exportButton.setOnAction(event -> {
ObservableList<TableColumn<FailedTradesListItem, ?>> tableColumns = tableView.getColumns();
int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon)
CSVEntryConverter<FailedTradesListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<FailedTradesListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View file

@ -23,6 +23,7 @@ import bisq.core.offer.Offer;
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 @@ class OpenOffersDataModel extends ActivatableDataModel {
list.sort((o1, o2) -> o2.getOffer().getDate().compareTo(o1.getOffer().getDate()));
}
boolean wasTriggered(OpenOffer openOffer) {
return TriggerPriceService.wasTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer);
}
}

View file

@ -47,13 +47,15 @@
<TableColumn fx:id="marketColumn" minWidth="75"/>
<TableColumn fx:id="priceColumn" minWidth="100"/>
<TableColumn fx:id="deviationColumn" minWidth="70"/>
<TableColumn fx:id="triggerPriceColumn" minWidth="90"/>
<TableColumn fx:id="amountColumn" minWidth="110"/>
<TableColumn fx:id="volumeColumn" minWidth="110"/>
<TableColumn fx:id="paymentMethodColumn" minWidth="120" maxWidth="170"/>
<TableColumn fx:id="directionColumn" minWidth="70"/>
<TableColumn fx:id="deactivateItemColumn" minWidth="60" maxWidth="60" sortable="false"/>
<TableColumn fx:id="editItemColumn" minWidth="50" maxWidth="60" sortable="false"/>
<TableColumn fx:id="removeItemColumn" minWidth="50" maxWidth="60" sortable="false"/>
<TableColumn fx:id="editItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
<TableColumn fx:id="triggerIconColumn" minWidth="30" maxWidth="30" sortable="false"/>
<TableColumn fx:id="removeItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
</columns>
</TableView>
<HBox spacing="10">

View file

@ -79,7 +79,7 @@ import java.util.Comparator;
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<VBox, OpenOffersViewModel> {
@ -89,7 +89,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
@FXML
TableColumn<OpenOfferListItem, OpenOfferListItem> priceColumn, deviationColumn, amountColumn, volumeColumn,
marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn,
removeItemColumn, editItemColumn, paymentMethodColumn;
removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn;
@FXML
HBox searchBox;
@FXML
@ -113,6 +113,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
private FilteredList<OpenOfferListItem> filteredList;
private ChangeListener<String> filterTextFieldListener;
private PortfolioView.OpenOfferActionHandler openOfferActionHandler;
private ChangeListener<Number> widthListener;
@Inject
public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) {
@ -123,6 +124,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
@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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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("openOffer.header.triggerPrice")));
deactivateItemColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.enabled")));
editItemColumn.setGraphic(new AutoTooltipLabel(""));
removeItemColumn.setGraphic(new AutoTooltipLabel(""));
@ -148,6 +151,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
setDateColumnCellFactory();
setDeactivateColumnCellFactory();
setEditColumnCellFactory();
setTriggerIconColumnCellFactory();
setTriggerPriceColumnCellFactory();
setRemoveColumnCellFactory();
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
@ -158,8 +163,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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,
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()));
@ -205,8 +210,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
exportButton.setOnAction(event -> {
ObservableList<TableColumn<OpenOfferListItem, ?>> tableColumns = tableView.getColumns();
int reportColumns = tableColumns.size() - 2; // CSV report excludes the last columns (icons)
CSVEntryConverter<OpenOfferListItem> headerConverter = transactionsListItem -> {
int reportColumns = tableColumns.size() - 3; // CSV report excludes the last columns (icons)
CSVEntryConverter<OpenOfferListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++) {
Node graphic = tableColumns.get(i).getGraphic();
@ -229,11 +234,12 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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;
};
@ -247,6 +253,18 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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() {
@ -266,14 +284,6 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
}
}
@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())
@ -314,6 +324,10 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
});
}
private void onWidthChange(double width) {
triggerPriceColumn.setVisible(width > 1200);
}
private void onDeactivateOpenOffer(OpenOffer openOffer) {
if (model.isBootstrappedOrShowPopup()) {
model.onDeactivateOpenOffer(openOffer,
@ -327,7 +341,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
}
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) -> {
@ -504,7 +518,33 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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);
}
}
};
}
});
}
private void setTriggerPriceColumnCellFactory() {
triggerPriceColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
triggerPriceColumn.setCellFactory(
new Callback<>() {
@Override
public TableCell<OpenOfferListItem, OpenOfferListItem> call(
TableColumn<OpenOfferListItem, OpenOfferListItem> 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);
}
@ -633,20 +673,23 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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);
@ -677,7 +720,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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);
}
@ -695,6 +738,48 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
});
}
private void setTriggerIconColumnCellFactory() {
triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
triggerIconColumn.setCellFactory(
new Callback<>() {
@Override
public TableCell<OpenOfferListItem, OpenOfferListItem> call(TableColumn<OpenOfferListItem, OpenOfferListItem> 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(
@ -710,7 +795,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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);
}

View file

@ -19,6 +19,7 @@ package bisq.desktop.main.portfolio.openoffer;
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 @@ import static com.google.common.base.Preconditions.checkNotNull;
class OpenOffersViewModel extends ActivatableWithDataModel<OpenOffersDataModel> 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<OpenOffersDataModel>
@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 @@ class OpenOffersViewModel extends ActivatableWithDataModel<OpenOffersDataModel>
}
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) {
@ -157,4 +171,18 @@ class OpenOffersViewModel extends ActivatableWithDataModel<OpenOffersDataModel>
btcFormatter.formatCoinWithCode(offer.getMakerFee()) :
bsqFormatter.formatCoinWithCode(offer.getMakerFee());
}
String getTriggerPrice(OpenOfferListItem item) {
if ((item == null)) {
return "";
}
Offer offer = item.getOffer();
long triggerPrice = item.getOpenOffer().getTriggerPrice();
if (!offer.isUseMarketBasedPrice() || triggerPrice <= 0) {
return Res.get("shared.na");
} else {
return PriceUtil.formatMarketPrice(triggerPrice, offer.getCurrencyCode());
}
}
}

View file

@ -44,6 +44,7 @@ import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.agent.DisputeAgentLookupMap;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.support.messages.ChatMessage;
@ -910,6 +911,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
TableColumn<Dispute, Dispute> roleColumn = getRoleColumn();
tableView.getColumns().add(roleColumn);
maybeAddAgentColumn();
stateColumn = getStateColumn();
tableView.getColumns().add(stateColumn);
@ -923,6 +926,15 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
tableView.getSortOrder().add(dateColumn);
}
protected void maybeAddAgentColumn() {
// Only relevant client views will impl it
}
// Relevant client views will override that
protected NodeAddress getAgentNodeAddress(Contract contract) {
return null;
}
private TableColumn<Dispute, Dispute> getSelectColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("shared.select"));
column.setMinWidth(80);
@ -1076,7 +1088,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private TableColumn<Dispute, Dispute> getBuyerOnionAddressColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.buyerAddress")) {
{
setMinWidth(190);
setMinWidth(160);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1102,7 +1114,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private TableColumn<Dispute, Dispute> getSellerOnionAddressColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.sellerAddress")) {
{
setMinWidth(190);
setMinWidth(160);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
@ -1216,6 +1228,40 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
return column;
}
protected TableColumn<Dispute, Dispute> getAgentColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.agent")) {
{
setMinWidth(70);
}
};
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<Dispute, Dispute> call(TableColumn<Dispute, Dispute> column) {
return new TableCell<>() {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
NodeAddress agentNodeAddress = getAgentNodeAddress(item.getContract());
if (agentNodeAddress == null) {
setText(Res.get("shared.na"));
return;
}
String keyBaseUserName = DisputeAgentLookupMap.getKeyBaseUserName(agentNodeAddress.getFullAddress());
setText(keyBaseUserName);
} else {
setText("");
}
}
};
}
});
return column;
}
private TableColumn<Dispute, Dispute> getStateColumn() {
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>(Res.get("support.state")) {
{

View file

@ -35,16 +35,21 @@ import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.mediation.MediationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.Contract;
import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.scene.control.TableColumn;
@FxmlView
public class MediationClientView extends DisputeClientView {
@Inject
@ -103,4 +108,15 @@ public class MediationClientView extends DisputeClientView {
.onAction(this::reOpenDispute)
.show();
}
@Override
protected NodeAddress getAgentNodeAddress(Contract contract) {
return contract.getMediatorNodeAddress();
}
@Override
protected void maybeAddAgentColumn() {
TableColumn<Dispute, Dispute> agentColumn = getAgentColumn();
tableView.getColumns().add(agentColumn);
}
}

View file

@ -33,16 +33,21 @@ import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.support.dispute.refund.RefundSession;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.Contract;
import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.scene.control.TableColumn;
@FxmlView
public class RefundClientView extends DisputeClientView {
@Inject
@ -73,4 +78,15 @@ public class RefundClientView extends DisputeClientView {
protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) {
return new RefundSession(dispute, disputeManager.isTrader(dispute));
}
@Override
protected NodeAddress getAgentNodeAddress(Contract contract) {
return contract.getRefundAgentNodeAddress();
}
@Override
protected void maybeAddAgentColumn() {
TableColumn<Dispute, Dispute> agentColumn = getAgentColumn();
tableView.getColumns().add(agentColumn);
}
}

View file

@ -1221,8 +1221,7 @@ public class FormBuilder {
int rowIndex,
String titleTextfield,
String titleCombobox
)
{
) {
return addTopLabelTextFieldAutocompleteComboBox(gridPane, rowIndex, titleTextfield, titleCombobox, 0);
}
@ -1232,8 +1231,7 @@ public class FormBuilder {
String titleTextfield,
String titleCombobox,
double top
)
{
) {
HBox hBox = new HBox();
hBox.setSpacing(10);
@ -2145,6 +2143,14 @@ public class FormBuilder {
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,

View file

@ -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.SepaAccountPayload;
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 class OfferBookViewModelTest {
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<PaymentAccount> paymentAccounts;
@ -229,7 +239,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
assertEquals(0, model.maxPlacesForAmount.intValue());
}
@ -243,7 +253,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
model.activate();
assertEquals(6, model.maxPlacesForAmount.intValue());
@ -261,7 +271,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
model.activate();
assertEquals(15, model.maxPlacesForAmount.intValue());
@ -280,7 +290,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
assertEquals(0, model.maxPlacesForVolume.intValue());
}
@ -294,7 +304,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
model.activate();
assertEquals(5, model.maxPlacesForVolume.intValue());
@ -312,7 +322,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
model.activate();
assertEquals(9, model.maxPlacesForVolume.intValue());
@ -331,7 +341,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
assertEquals(0, model.maxPlacesForPrice.intValue());
}
@ -345,7 +355,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
model.activate();
assertEquals(7, model.maxPlacesForPrice.intValue());
@ -363,7 +373,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue());
}
@ -391,7 +401,7 @@ public class OfferBookViewModelTest {
offerBookListItems.addAll(item1, item2);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, priceFeedService,
null, null, null, null, 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 class OfferBookViewModelTest {
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, null, null, coinFormatter, new BsqFormatter());
null, null, null, null, getPriceUtil(), coinFormatter, new BsqFormatter());
final OfferBookListItem item = make(btcBuyItem.but(
with(useMarketBasedPrice, true),

View file

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