diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
index 6ae03a7042..a08b767a47 100644
--- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java
+++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
@@ -1396,6 +1396,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
+ // verify max length of extra info
+ if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
+ errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
// verify the trade protocol version
if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) {
errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion();
diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java
index b270762d3b..aefb92c41a 100644
--- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java
+++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java
@@ -30,6 +30,7 @@ public class Restrictions {
public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5;
public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1);
public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1);
+ public static int MAX_EXTRA_INFO_LENGTH = 1500;
// At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the
// mediated payout. For Refund agent cases we do not have that restriction.
diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties
index cb6a632270..82a09919ba 100644
--- a/core/src/main/resources/i18n/displayStrings.properties
+++ b/core/src/main/resources/i18n/displayStrings.properties
@@ -495,6 +495,7 @@ createOffer.triggerPrice.tooltip=As protection against drastic price movements y
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}
+createOffer.extraInfo.invalid.tooLong=Must not exceed {0} characters.
# new entries
createOffer.placeOfferButton=Review: Place offer to {0} monero
diff --git a/desktop/src/main/java/haveno/desktop/components/InputTextArea.java b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java
new file mode 100644
index 0000000000..7bcd18de93
--- /dev/null
+++ b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java
@@ -0,0 +1,140 @@
+/*
+ * This file is part of Haveno.
+ *
+ * Haveno 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.
+ *
+ * Haveno 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 Haveno. If not, see .
+ */
+
+package haveno.desktop.components;
+
+
+import com.jfoenix.controls.JFXTextArea;
+import haveno.core.util.validation.InputValidator;
+import haveno.desktop.util.validation.JFXInputValidator;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.Skin;
+
+/**
+ * TextArea with validation support.
+ * If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is
+ * needed the validationResultProperty can be used for applying validation result done by external validation.
+ * In case the isValid property in validationResultProperty get set to false we display a red border and an error
+ * message within the errorMessageDisplay placed on the right of the text area.
+ * The errorMessageDisplay gets closed when the ValidatingTextArea instance gets removed from the scene graph or when
+ * hideErrorMessageDisplay() is called.
+ * There can be only 1 errorMessageDisplays at a time we use static field for it.
+ * The position is derived from the position of the textArea itself or if set from the layoutReference node.
+ */
+//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements.
+public class InputTextArea extends JFXTextArea {
+
+ private final ObjectProperty validationResult = new SimpleObjectProperty<>
+ (new InputValidator.ValidationResult(true));
+
+ private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator();
+
+ private InputValidator validator;
+ private String errorMessage = null;
+
+
+ public InputValidator getValidator() {
+ return validator;
+ }
+
+ public void setValidator(InputValidator validator) {
+ this.validator = validator;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Constructor
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public InputTextArea() {
+ super();
+
+ getValidators().add(jfxValidationWrapper);
+
+ validationResult.addListener((ov, oldValue, newValue) -> {
+ if (newValue != null) {
+ jfxValidationWrapper.resetValidation();
+ if (!newValue.isValid) {
+ if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking
+ validate(); // ensure that the new error message replaces the old one
+ }
+ if (this.errorMessage != null) {
+ jfxValidationWrapper.applyErrorMessage(this.errorMessage);
+ } else {
+ jfxValidationWrapper.applyErrorMessage(newValue);
+ }
+ }
+ validate();
+ }
+ });
+
+ textProperty().addListener((o, oldValue, newValue) -> {
+ refreshValidation();
+ });
+
+ focusedProperty().addListener((o, oldValue, newValue) -> {
+ if (validator != null) {
+ if (!oldValue && newValue) {
+ this.validationResult.set(new InputValidator.ValidationResult(true));
+ } else {
+ this.validationResult.set(validator.validate(getText()));
+ }
+ }
+ });
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Public methods
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public void resetValidation() {
+ jfxValidationWrapper.resetValidation();
+
+ String input = getText();
+ if (input.isEmpty()) {
+ validationResult.set(new InputValidator.ValidationResult(true));
+ } else {
+ validationResult.set(validator.validate(input));
+ }
+ }
+
+ public void refreshValidation() {
+ if (validator != null) {
+ this.validationResult.set(validator.validate(getText()));
+ }
+ }
+
+ public void setInvalid(String message) {
+ validationResult.set(new InputValidator.ValidationResult(false, message));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Getters
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public ObjectProperty validationResultProperty() {
+ return validationResult;
+ }
+
+ protected Skin> createDefaultSkin() {
+ return new JFXTextAreaSkinHavenoStyle(this);
+ }
+}
diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css
index fc92b1c308..2f50f8d0f3 100644
--- a/desktop/src/main/java/haveno/desktop/haveno.css
+++ b/desktop/src/main/java/haveno/desktop/haveno.css
@@ -501,15 +501,15 @@ tree-table-view:focused {
-jfx-default-color: -bs-color-primary;
}
-.jfx-date-picker .jfx-text-field {
+.jfx-date-picker .jfx-text-field .jfx-text-area {
-fx-padding: 0.333333em 0em 0.333333em 0em;
}
-.jfx-date-picker .jfx-text-field > .input-line {
+.jfx-date-picker .jfx-text-field .jfx-text-area > .input-line {
-fx-translate-x: 0em;
}
-.jfx-date-picker .jfx-text-field > .input-focused-line {
+.jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line {
-fx-translate-x: 0em;
}
diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java
index e3f9132a84..1ed15ad845 100644
--- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java
+++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java
@@ -44,8 +44,8 @@ import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.BalanceTextField;
import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.FundsTextField;
-import haveno.desktop.components.HavenoTextArea;
import haveno.desktop.components.InfoInputTextField;
+import haveno.desktop.components.InputTextArea;
import haveno.desktop.components.InputTextField;
import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.main.MainView;
@@ -76,7 +76,6 @@ import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
-import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
@@ -140,7 +139,7 @@ public abstract class MutableOfferView> exten
private BalanceTextField balanceTextField;
private ToggleButton reserveExactAmountSlider;
private ToggleButton buyerAsTakerWithoutDepositSlider;
- protected TextArea extraInfoTextArea;
+ protected InputTextArea extraInfoTextArea;
private FundsTextField totalToPayTextField;
private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel,
waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel,
@@ -211,7 +210,7 @@ public abstract class MutableOfferView> exten
createListeners();
- balanceTextField.setFormatter(model.getBtcFormatter());
+ balanceTextField.setFormatter(model.getXmrFormatter());
paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter());
paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"),
@@ -592,6 +591,7 @@ public abstract class MutableOfferView> exten
triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult);
volumeTextField.validationResultProperty().bind(model.volumeValidationResult);
securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult);
+ extraInfoTextArea.validationResultProperty().bind(model.extraInfoValidationResult);
// funding
fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed));
@@ -713,7 +713,7 @@ public abstract class MutableOfferView> exten
triggerPriceInputTextField.setText(model.triggerPrice.get());
};
extraInfoFocusedListener = (observable, oldValue, newValue) -> {
- model.onFocusOutExtraInfoTextField(oldValue, newValue);
+ model.onFocusOutExtraInfoTextArea(oldValue, newValue);
extraInfoTextArea.setText(model.extraInfo.get());
};
@@ -1097,7 +1097,7 @@ public abstract class MutableOfferView> exten
Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment);
GridPane.setColumnSpan(extraInfoTitledGroupBg, 3);
- extraInfoTextArea = new HavenoTextArea();
+ extraInfoTextArea = new InputTextArea();
extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer"));
extraInfoTextArea.getStyleClass().add("text-area");
extraInfoTextArea.setWrapText(true);
@@ -1109,7 +1109,7 @@ public abstract class MutableOfferView> exten
GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING);
GridPane.setColumnIndex(extraInfoTextArea, 0);
GridPane.setHalignment(extraInfoTextArea, HPos.LEFT);
- GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
+ GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 10, 0));
gridPane.getChildren().add(extraInfoTextArea);
}
diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java
index e32869afe2..6d087b27ea 100644
--- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java
+++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java
@@ -99,7 +99,7 @@ public abstract class MutableOfferViewModel ext
private final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation;
private final Preferences preferences;
- protected final CoinFormatter btcFormatter;
+ protected final CoinFormatter xmrFormatter;
private final FiatVolumeValidator fiatVolumeValidator;
private final AmountValidator4Decimals amountValidator4Decimals;
private final AmountValidator8Decimals amountValidator8Decimals;
@@ -160,6 +160,7 @@ public abstract class MutableOfferViewModel ext
final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true));
final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>();
final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>();
+ final ObjectProperty extraInfoValidationResult = new SimpleObjectProperty<>();
private ChangeListener amountStringListener;
private ChangeListener minAmountStringListener;
@@ -195,26 +196,26 @@ public abstract class MutableOfferViewModel ext
FiatVolumeValidator fiatVolumeValidator,
AmountValidator4Decimals amountValidator4Decimals,
AmountValidator8Decimals amountValidator8Decimals,
- XmrValidator btcValidator,
+ XmrValidator xmrValidator,
SecurityDepositValidator securityDepositValidator,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
Preferences preferences,
- @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
+ @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter,
OfferUtil offerUtil) {
super(dataModel);
this.fiatVolumeValidator = fiatVolumeValidator;
this.amountValidator4Decimals = amountValidator4Decimals;
this.amountValidator8Decimals = amountValidator8Decimals;
- this.xmrValidator = btcValidator;
+ this.xmrValidator = xmrValidator;
this.securityDepositValidator = securityDepositValidator;
this.priceFeedService = priceFeedService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.navigation = navigation;
this.preferences = preferences;
- this.btcFormatter = btcFormatter;
+ this.xmrFormatter = xmrFormatter;
this.offerUtil = offerUtil;
paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId);
@@ -500,11 +501,7 @@ public abstract class MutableOfferViewModel ext
};
extraInfoStringListener = (ov, oldValue, newValue) -> {
- if (newValue != null) {
- extraInfo.set(newValue);
- } else {
- extraInfo.set("");
- }
+ onExtraInfoTextAreaChanged();
};
isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState();
@@ -531,7 +528,7 @@ public abstract class MutableOfferViewModel ext
tradeFee.set(HavenoUtils.formatXmr(makerFee));
tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getMaxMakerFee(),
- btcFormatter));
+ xmrFormatter));
}
@@ -836,8 +833,16 @@ public abstract class MutableOfferViewModel ext
}
}
- public void onFocusOutExtraInfoTextField(boolean oldValue, boolean newValue) {
+ public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) {
if (oldValue && !newValue) {
+ onExtraInfoTextAreaChanged();
+ }
+ }
+
+ public void onExtraInfoTextAreaChanged() {
+ extraInfoValidationResult.set(getExtraInfoValidationResult());
+ updateButtonDisableState();
+ if (extraInfoValidationResult.get().isValid) {
dataModel.setExtraInfo(extraInfo.get());
}
}
@@ -1045,8 +1050,8 @@ public abstract class MutableOfferViewModel ext
.show();
}
- CoinFormatter getBtcFormatter() {
- return btcFormatter;
+ CoinFormatter getXmrFormatter() {
+ return xmrFormatter;
}
public boolean isShownAsBuyOffer() {
@@ -1064,7 +1069,7 @@ public abstract class MutableOfferViewModel ext
public String getTradeAmount() {
return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getAmount().get(),
- btcFormatter);
+ xmrFormatter);
}
public String getSecurityDepositLabel() {
@@ -1084,7 +1089,7 @@ public abstract class MutableOfferViewModel ext
return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getSecurityDeposit(),
dataModel.getAmount().get(),
- btcFormatter
+ xmrFormatter
);
}
@@ -1097,7 +1102,7 @@ public abstract class MutableOfferViewModel ext
return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getMaxMakerFee(),
dataModel.getAmount().get(),
- btcFormatter);
+ xmrFormatter);
}
public String getMakerFeePercentage() {
@@ -1108,7 +1113,7 @@ public abstract class MutableOfferViewModel ext
public String getTotalToPayInfo() {
return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.totalToPay.get(),
- btcFormatter);
+ xmrFormatter);
}
public String getFundsStructure() {
@@ -1181,7 +1186,7 @@ public abstract class MutableOfferViewModel ext
private void setAmountToModel() {
if (amount.get() != null && !amount.get().isEmpty()) {
- BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), btcFormatter));
+ BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter));
long maxTradeLimit = dataModel.getMaxTradeLimit();
Price price = dataModel.getPrice().get();
@@ -1202,7 +1207,7 @@ public abstract class MutableOfferViewModel ext
private void setMinAmountToModel() {
if (minAmount.get() != null && !minAmount.get().isEmpty()) {
- BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), btcFormatter));
+ BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter));
Price price = dataModel.getPrice().get();
long maxTradeLimit = dataModel.getMaxTradeLimit();
@@ -1343,10 +1348,20 @@ public abstract class MutableOfferViewModel ext
inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid;
}
+ inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid;
+
isNextButtonDisabled.set(!inputDataValid);
isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get());
}
+ private ValidationResult getExtraInfoValidationResult() {
+ if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
+ return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.MAX_EXTRA_INFO_LENGTH));
+ } else {
+ return new InputValidator.ValidationResult(true);
+ }
+ }
+
private void updateMarketPriceToManual() {
final String currencyCode = dataModel.getTradeCurrencyCode().get();
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);