From 1a2dcfc704a5d5a90a6c155fe54a9dfc38d2d8b2 Mon Sep 17 00:00:00 2001 From: woodser <woodser@protonmail.com> Date: Sat, 22 Mar 2025 07:57:31 -0400 Subject: [PATCH] limit offer extra info to 1500 characters --- .../haveno/core/offer/OpenOfferManager.java | 8 + .../haveno/core/xmr/wallet/Restrictions.java | 1 + .../resources/i18n/displayStrings.properties | 1 + .../desktop/components/InputTextArea.java | 140 ++++++++++++++++++ .../src/main/java/haveno/desktop/haveno.css | 6 +- .../desktop/main/offer/MutableOfferView.java | 14 +- .../main/offer/MutableOfferViewModel.java | 55 ++++--- 7 files changed, 195 insertions(+), 30 deletions(-) create mode 100644 desktop/src/main/java/haveno/desktop/components/InputTextArea.java 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 <http://www.gnu.org/licenses/>. + */ + +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<InputValidator.ValidationResult> 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<InputValidator.ValidationResult> 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<M extends MutableOfferViewModel<?>> 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<M extends MutableOfferViewModel<?>> 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<M extends MutableOfferViewModel<?>> 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<M extends MutableOfferViewModel<?>> 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<M extends MutableOfferViewModel<?>> 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<M extends MutableOfferViewModel<?>> 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<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> ext final ObjectProperty<InputValidator.ValidationResult> triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>(); final ObjectProperty<InputValidator.ValidationResult> securityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty<InputValidator.ValidationResult> extraInfoValidationResult = new SimpleObjectProperty<>(); private ChangeListener<String> amountStringListener; private ChangeListener<String> minAmountStringListener; @@ -195,26 +196,26 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> ext tradeFee.set(HavenoUtils.formatXmr(makerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMaxMakerFee(), - btcFormatter)); + xmrFormatter)); } @@ -836,8 +833,16 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> ext .show(); } - CoinFormatter getBtcFormatter() { - return btcFormatter; + CoinFormatter getXmrFormatter() { + return xmrFormatter; } public boolean isShownAsBuyOffer() { @@ -1064,7 +1069,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext public String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getSecurityDepositLabel() { @@ -1084,7 +1089,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter + xmrFormatter ); } @@ -1097,7 +1102,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMaxMakerFee(), dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getMakerFeePercentage() { @@ -1108,7 +1113,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext public String getTotalToPayInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.totalToPay.get(), - btcFormatter); + xmrFormatter); } public String getFundsStructure() { @@ -1181,7 +1186,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> 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<M extends MutableOfferDataModel> 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);