limit offer extra info to 1500 characters

This commit is contained in:
woodser 2025-03-21 12:51:01 -04:00
parent ce27818f43
commit 2b086a4487
No known key found for this signature in database
GPG key ID: 55A10DD48ADEE5EF
7 changed files with 195 additions and 30 deletions
core/src/main
java/haveno/core
resources/i18n
desktop/src/main/java/haveno/desktop

View file

@ -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();

View file

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

View file

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

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);