From 5af9a9a8abad058177981166a977e58a0b9b3529 Mon Sep 17 00:00:00 2001 From: walkerp07 Date: Mon, 1 Jul 2024 09:54:41 -0400 Subject: [PATCH] Implement rounding options for "Pay-By-Mail" payment type --- .../main/java/haveno/core/api/CoreApi.java | 4 ++ .../haveno/core/api/CoreOffersService.java | 4 ++ .../java/haveno/core/api/model/OfferInfo.java | 5 ++ .../api/model/builder/OfferInfoBuilder.java | 8 ++- .../haveno/core/offer/CreateOfferService.java | 4 ++ .../main/java/haveno/core/offer/Offer.java | 4 ++ .../haveno/core/offer/OfferBookService.java | 1 + .../java/haveno/core/offer/OfferForJson.java | 7 ++ .../java/haveno/core/offer/OfferPayload.java | 8 +++ .../haveno/core/offer/OpenOfferManager.java | 5 +- .../core/payment/payload/PaymentMethod.java | 6 +- .../java/haveno/core/util/VolumeUtil.java | 36 ++++++++--- .../resources/i18n/displayStrings.properties | 1 + .../java/haveno/core/offer/OfferMaker.java | 1 + .../haveno/daemon/grpc/GrpcOffersService.java | 1 + .../main/offer/MutableOfferDataModel.java | 34 ++++++++-- .../desktop/main/offer/MutableOfferView.java | 48 +++++++++++++- .../main/offer/MutableOfferViewModel.java | 64 +++++++++++++++++++ .../DuplicateOfferDataModel.java | 1 + .../editoffer/EditOfferDataModel.java | 5 +- .../portfolio/editoffer/EditOfferView.java | 2 + .../trades/TradesChartsViewModelTest.java | 1 + .../offerbook/OfferBookViewModelTest.java | 1 + .../java/haveno/desktop/maker/OfferMaker.java | 2 + proto/src/main/proto/grpc.proto | 58 +++++++++-------- proto/src/main/proto/pb.proto | 57 +++++++++-------- 26 files changed, 292 insertions(+), 76 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index b4a293a3b91..f49ca0d1255 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -415,6 +415,7 @@ public void postOffer(String currencyCode, double marketPriceMargin, long amountAsLong, long minAmountAsLong, + int roundTo, double buyerSecurityDeposit, String triggerPriceAsString, boolean reserveExactAmount, @@ -428,6 +429,7 @@ public void postOffer(String currencyCode, marketPriceMargin, amountAsLong, minAmountAsLong, + roundTo, buyerSecurityDeposit, triggerPriceAsString, reserveExactAmount, @@ -444,6 +446,7 @@ public Offer editOffer(String offerId, double marketPriceMargin, BigInteger amount, BigInteger minAmount, + Integer roundTo, double buyerSecurityDeposit, PaymentAccount paymentAccount) { return coreOffersService.editOffer(offerId, @@ -454,6 +457,7 @@ public Offer editOffer(String offerId, marketPriceMargin, amount, minAmount, + roundTo, buyerSecurityDeposit, paymentAccount); } diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 7e67965c9ce..bda170a60b5 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -172,6 +172,7 @@ void postOffer(String currencyCode, double marketPriceMargin, long amountAsLong, long minAmountAsLong, + int roundTo, double securityDeposit, String triggerPriceAsString, boolean reserveExactAmount, @@ -196,6 +197,7 @@ void postOffer(String currencyCode, upperCaseCurrencyCode, amount, minAmount, + roundTo, price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), @@ -223,6 +225,7 @@ Offer editOffer(String offerId, double marketPriceMargin, BigInteger amount, BigInteger minAmount, + Integer roundTo, double buyerSecurityDeposit, PaymentAccount paymentAccount) { return createOfferService.createAndGetOffer(offerId, @@ -230,6 +233,7 @@ Offer editOffer(String offerId, currencyCode.toUpperCase(), amount, minAmount, + roundTo, price, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java index b489aaa8bb8..6179cb47d2d 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -50,6 +50,7 @@ public class OfferInfo implements Payload { private final double marketPriceMarginPct; private final long amount; private final long minAmount; + private final int roundTo; private final String volume; private final String minVolume; private final double makerFeePct; @@ -87,6 +88,7 @@ public OfferInfo(OfferInfoBuilder builder) { this.marketPriceMarginPct = builder.getMarketPriceMarginPct(); this.amount = builder.getAmount(); this.minAmount = builder.getMinAmount(); + this.roundTo = builder.getRoundTo(); this.makerFeePct = builder.getMakerFeePct(); this.takerFeePct = builder.getTakerFeePct(); this.penaltyFeePct = builder.getPenaltyFeePct(); @@ -158,6 +160,7 @@ private static OfferInfoBuilder getBuilder(Offer offer) { .withMarketPriceMarginPct(marketPriceMarginAsPctLiteral) .withAmount(offer.getAmount().longValueExact()) .withMinAmount(offer.getMinAmount().longValueExact()) + .withRoundTo(offer.getRoundToSelection()) .withMakerFeePct(offer.getMakerFeePct()) .withTakerFeePct(offer.getTakerFeePct()) .withPenaltyFeePct(offer.getPenaltyFeePct()) @@ -194,6 +197,7 @@ public haveno.proto.grpc.OfferInfo toProtoMessage() { .setMarketPriceMarginPct(marketPriceMarginPct) .setAmount(amount) .setMinAmount(minAmount) + .setRoundTo(roundTo) .setVolume(volume) .setMinVolume(minVolume) .setMakerFeePct(makerFeePct) @@ -231,6 +235,7 @@ public static OfferInfo fromProto(haveno.proto.grpc.OfferInfo proto) { .withMarketPriceMarginPct(proto.getMarketPriceMarginPct()) .withAmount(proto.getAmount()) .withMinAmount(proto.getMinAmount()) + .withRoundTo(proto.getRoundTo()) .withVolume(proto.getVolume()) .withMinVolume(proto.getMinVolume()) .withMakerFeePct(proto.getMakerFeePct()) diff --git a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java index 35d532f67ff..92cf9c38ad7 100644 --- a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java +++ b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java @@ -36,6 +36,7 @@ public final class OfferInfoBuilder { private double marketPriceMarginPct; private long amount; private long minAmount; + private int roundTo; private String volume; private String minVolume; private double makerFeePct; @@ -99,6 +100,11 @@ public OfferInfoBuilder withMinAmount(long minAmount) { return this; } + public OfferInfoBuilder withRoundTo(int roundTo) { + this.roundTo = roundTo; + return this; + } + public OfferInfoBuilder withMakerFeePct(double makerFeePct) { this.makerFeePct = makerFeePct; return this; @@ -223,7 +229,7 @@ public OfferInfoBuilder withArbitratorSigner(String arbitratorSigner) { this.arbitratorSigner = arbitratorSigner; return this; } - + public OfferInfoBuilder withSplitOutputTxHash(String splitOutputTxHash) { this.splitOutputTxHash = splitOutputTxHash; return this; diff --git a/core/src/main/java/haveno/core/offer/CreateOfferService.java b/core/src/main/java/haveno/core/offer/CreateOfferService.java index afd4366a3b4..3dc214215b2 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -99,6 +99,7 @@ public Offer createAndGetOffer(String offerId, String currencyCode, BigInteger amount, BigInteger minAmount, + Integer roundTo, Price fixedPrice, boolean useMarketBasedPrice, double marketPriceMargin, @@ -109,6 +110,7 @@ public Offer createAndGetOffer(String offerId, "currencyCode={}, " + "direction={}, " + "fixedPrice={}, " + + "roundTo={}, " + "useMarketBasedPrice={}, " + "marketPriceMargin={}, " + "amount={}, " + @@ -118,6 +120,7 @@ public Offer createAndGetOffer(String offerId, currencyCode, direction, fixedPrice == null ? null : fixedPrice.getValue(), + roundTo, useMarketBasedPrice, marketPriceMargin, amount, @@ -189,6 +192,7 @@ public Offer createAndGetOffer(String offerId, useMarketBasedPriceValue, amountAsLong, minAmountAsLong, + roundTo, HavenoUtils.MAKER_FEE_PCT, HavenoUtils.TAKER_FEE_PCT, HavenoUtils.PENALTY_FEE_PCT, diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 5c2f6eb51b4..275f81858ec 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -349,6 +349,10 @@ public BigInteger getMinAmount() { return BigInteger.valueOf(offerPayload.getMinAmount()); } + public Integer getRoundToSelection() { + return offerPayload.getRoundTo(); + } + public boolean isRange() { return offerPayload.getAmount() != offerPayload.getMinAmount(); } diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index 4e7adbb52e2..1bea462bb4d 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -332,6 +332,7 @@ private void doDumpStatistics() { return new OfferForJson(offer.getDirection(), offer.getCurrencyCode(), offer.getMinAmount(), + offer.getRoundToSelection(), offer.getAmount(), offer.getPrice(), offer.getDate(), diff --git a/core/src/main/java/haveno/core/offer/OfferForJson.java b/core/src/main/java/haveno/core/offer/OfferForJson.java index caebcdc3dd1..e4ab79bbf4d 100644 --- a/core/src/main/java/haveno/core/offer/OfferForJson.java +++ b/core/src/main/java/haveno/core/offer/OfferForJson.java @@ -26,6 +26,8 @@ import haveno.core.monetary.Volume; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; +import lombok.Getter; +import lombok.Setter; import org.bitcoinj.utils.MonetaryFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +42,9 @@ public class OfferForJson { public final OfferDirection direction; public final String currencyCode; public final long minAmount; + @Getter + @Setter + public final int roundTo; public final long amount; public final long price; public final long date; @@ -75,6 +80,7 @@ public class OfferForJson { public OfferForJson(OfferDirection direction, String currencyCode, BigInteger minAmount, + Integer roundTo, BigInteger amount, @Nullable Price price, Date date, @@ -86,6 +92,7 @@ public OfferForJson(OfferDirection direction, this.direction = direction; this.currencyCode = currencyCode; this.minAmount = minAmount.longValueExact(); + this.roundTo = roundTo; this.amount = amount.longValueExact(); this.price = price.getValue(); this.date = date.getTime(); diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 1a194a1a98b..0e8a1118e5e 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -67,6 +67,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay protected final long price; protected final long amount; protected final long minAmount; + protected final int roundTo; protected final String paymentMethodId; protected final String makerPaymentAccountId; protected final NodeAddress ownerNodeAddress; @@ -171,6 +172,7 @@ public OfferPayload(String id, boolean useMarketBasedPrice, long amount, long minAmount, + int roundTo, double makerFeePct, double takerFeePct, double penaltyFeePct, @@ -209,6 +211,7 @@ public OfferPayload(String id, this.price = price; this.amount = amount; this.minAmount = minAmount; + this.roundTo = roundTo; this.makerFeePct = makerFeePct; this.takerFeePct = takerFeePct; this.penaltyFeePct = penaltyFeePct; @@ -260,6 +263,7 @@ public byte[] getSignatureHash() { false, amount, minAmount, + roundTo, makerFeePct, takerFeePct, penaltyFeePct, @@ -350,6 +354,7 @@ public protobuf.StoragePayload toProtoMessage() { .setUseMarketBasedPrice(useMarketBasedPrice) .setAmount(amount) .setMinAmount(minAmount) + .setRoundTo(roundTo) .setMakerFeePct(makerFeePct) .setTakerFeePct(takerFeePct) .setPenaltyFeePct(penaltyFeePct) @@ -402,6 +407,7 @@ public static OfferPayload fromProto(protobuf.OfferPayload proto) { proto.getUseMarketBasedPrice(), proto.getAmount(), proto.getMinAmount(), + proto.getRoundTo(), proto.getMakerFeePct(), proto.getTakerFeePct(), proto.getPenaltyFeePct(), @@ -442,6 +448,7 @@ public String toString() { ",\r\n price=" + price + ",\r\n amount=" + amount + ",\r\n minAmount=" + minAmount + + ",\r\n roundTo=" + roundTo + ",\r\n makerFeePct=" + makerFeePct + ",\r\n takerFeePct=" + takerFeePct + ",\r\n penaltyFeePct=" + penaltyFeePct + @@ -493,6 +500,7 @@ public JsonElement serialize(OfferPayload offerPayload, Type type, JsonSerializa object.add("useMarketBasedPrice", context.serialize(offerPayload.isUseMarketBasedPrice())); object.add("amount", context.serialize(offerPayload.getAmount())); object.add("minAmount", context.serialize(offerPayload.getMinAmount())); + object.add("roundTo", context.serialize(offerPayload.getRoundTo())); object.add("makerFeePct", context.serialize(offerPayload.getMakerFeePct())); object.add("takerFeePct", context.serialize(offerPayload.getTakerFeePct())); object.add("penaltyFeePct", context.serialize(offerPayload.getPenaltyFeePct())); diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index f507cd9758d..0b39ab2e229 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -715,7 +715,7 @@ private void doCancelOffer(@NotNull OpenOffer openOffer) { Offer offer = openOffer.getOffer(); offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); - removeOpenOffer(openOffer); + removeOpenOffer(openOffer); closedTradableManager.add(openOffer); // TODO: don't add these to closed tradables? xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); requestPersistence(); @@ -893,7 +893,7 @@ private void processScheduledOffers(TransactionResultHandler resultHandler, // T } private void processUnpostedOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - + // skip if already processing if (openOffer.isProcessing()) { resultHandler.handleResult(null); @@ -1615,6 +1615,7 @@ private void maybeUpdatePersistedOffers() { originalOfferPayload.isUseMarketBasedPrice(), originalOfferPayload.getAmount(), originalOfferPayload.getMinAmount(), + originalOfferPayload.getRoundTo(), originalOfferPayload.getMakerFeePct(), originalOfferPayload.getTakerFeePct(), originalOfferPayload.getPenaltyFeePct(), diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index cf75df36523..a44ec2a54e9 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -343,7 +343,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable price = new SimpleObjectProperty<>(); protected final ObjectProperty volume = new SimpleObjectProperty<>(); protected final ObjectProperty minVolume = new SimpleObjectProperty<>(); + protected final ObjectProperty roundTo = new SimpleObjectProperty<>(); // Percentage value of buyer security deposit. E.g. 0.01 means 1% of trade amount protected final DoubleProperty buyerSecurityDepositPct = new SimpleDoubleProperty(); @@ -284,6 +288,7 @@ protected Offer createAndGetOffer() { tradeCurrencyCode.get(), amount.get(), minAmount.get(), + roundTo.get(), useMarketBasedPrice.get() ? null : price.get(), useMarketBasedPrice.get(), useMarketBasedPrice.get() ? marketPriceMargin : 0, @@ -361,6 +366,15 @@ private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) { tradeCurrencyCode.set(tradeCurrency.getCode()); } + protected void onRoundingSelected(Integer rounding) { + roundTo.set(rounding); + + calculateVolume(); + calculateTotalToPay(); + updateBalance(); + setSuggestedSecurityDeposit(getPaymentAccount()); + } + void onCurrencySelected(TradeCurrency tradeCurrency) { if (tradeCurrency != null) { if (!this.tradeCurrency.equals(tradeCurrency)) { @@ -448,6 +462,10 @@ public ObservableList getPaymentAccounts() { return paymentAccounts; } + public ObservableList getRoundingList() { + // TODO: Do not hard code + return FXCollections.observableArrayList(1, 5, 10, 20); + } public double getMarketPriceMarginPct() { return marketPriceMargin; } @@ -488,7 +506,6 @@ void calculateVolume() { if (isNonZeroPrice.test(price) && isNonZeroAmount.test(amount)) { try { Volume volumeByAmount = calculateVolumeForAmount(amount); - volume.set(volumeByAmount); calculateMinVolume(); @@ -504,7 +521,6 @@ void calculateMinVolume() { if (isNonZeroPrice.test(price) && isNonZeroAmount.test(minAmount)) { try { Volume volumeByAmount = calculateVolumeForAmount(minAmount); - minVolume.set(volumeByAmount); } catch (Throwable t) { @@ -515,7 +531,7 @@ void calculateMinVolume() { private Volume calculateVolumeForAmount(ObjectProperty minAmount) { Volume volumeByAmount = price.get().getVolumeByAmount(minAmount.get()); - volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, paymentAccount.getPaymentMethod().getId()); + volumeByAmount = VolumeUtil.getAdjustedVolume(volumeByAmount, paymentAccount.getPaymentMethod().getId(), roundTo.get()); return volumeByAmount; } @@ -602,6 +618,10 @@ protected ReadOnlyObjectProperty getMinAmount() { return minAmount; } + protected ReadOnlyObjectProperty getRoundToSelection() { + return roundTo; + } + public ReadOnlyObjectProperty getPrice() { return price; } @@ -618,6 +638,10 @@ protected void setMinAmount(BigInteger minAmount) { this.minAmount.set(minAmount); } + protected void setRoundToSelection(Integer selection) { + this.roundTo.set(selection); + } + public ReadOnlyStringProperty getTradeCurrencyCode() { return tradeCurrencyCode; } 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 53df033ed19..bd89fc43bc1 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -55,6 +55,7 @@ import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.QRCodeWindow; import haveno.desktop.main.portfolio.PortfolioView; +import haveno.desktop.main.portfolio.editoffer.EditOfferView; import haveno.desktop.main.portfolio.openoffer.OpenOffersView; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; @@ -145,6 +146,7 @@ public abstract class MutableOfferView> exten protected Label amountBtcLabel, volumeCurrencyLabel, minAmountBtcLabel; private ComboBox paymentAccountsComboBox; private ComboBox currencyComboBox; + protected ComboBox roundToComboBox; private ImageView qrCodeImageView; private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox; private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox, @@ -155,12 +157,13 @@ public abstract class MutableOfferView> exten private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener, buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener, priceAsPercentageFocusedListener, getShowWalletFundedNotificationListener, - isMinBuyerSecurityDepositListener, triggerPriceFocusedListener; + isMinBuyerSecurityDepositListener, triggerPriceFocusedListener, roundToSelectionFocusedListener; private ChangeListener missingCoinListener; private ChangeListener tradeCurrencyCodeListener, errorMessageListener, marketPriceMarginListener, volumeListener, buyerSecurityDepositInBTCListener; private ChangeListener marketPriceAvailableListener; private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler; + private EventHandler roundToComboBoxSelectionHandler; private OfferView.CloseHandler closeHandler; protected int gridRow = 0; @@ -228,6 +231,7 @@ protected void doActivate() { isActivated = true; currencyComboBox.setPrefWidth(250); paymentAccountsComboBox.setPrefWidth(250); + roundToComboBox.setPrefWidth(100); addBindings(); addListeners(); @@ -244,6 +248,7 @@ protected void doActivate() { currencyComboBox.getSelectionModel().select(model.getTradeCurrency()); paymentAccountsComboBox.setItems(getPaymentAccounts()); paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount()); + roundToComboBox.setItems(getRoundingList()); onPaymentAccountsComboBoxSelected(); @@ -403,6 +408,7 @@ private void onShowPayFundsScreen() { }); paymentAccountsComboBox.setDisable(true); + roundToComboBox.setDisable(true); editOfferElements.forEach(node -> { node.setMouseTransparent(true); @@ -507,6 +513,14 @@ protected void onPaymentAccountsComboBoxSelected() { if (singleTradeCurrency != null) currencyTextField.setText(singleTradeCurrency.getNameAndCode()); } + if (!(this instanceof EditOfferView)) + if (PaymentMethod.isRoundedForPBMCash(paymentAccount.getPaymentMethod().getId())) { + roundToComboBox.setDisable(false); + } else { + roundToComboBox.setDisable(true); + model.roundTo.set(1); // Initialize model data with '1' for any non-PBM payment type + } + } else { currencySelection.setVisible(false); currencySelection.setManaged(false); @@ -524,6 +538,10 @@ private void onCurrencyComboBoxSelected() { model.onCurrencySelected(currencyComboBox.getSelectionModel().getSelectedItem()); } + protected void onRoundToComboBoxSelected() { + model.onRoundingSelected(roundToComboBox.getSelectionModel().getSelectedItem()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Navigation /////////////////////////////////////////////////////////////////////////////////////////// @@ -560,6 +578,7 @@ private void addBindings() { buyerSecurityDepositLabel.textProperty().bind(model.buyerSecurityDepositLabel); tradeFeeInXmrLabel.textProperty().bind(model.tradeFeeInXmrWithFiat); tradeFeeDescriptionLabel.textProperty().bind(model.tradeFeeDescription); + roundToComboBox.valueProperty().bindBidirectional(model.roundTo); // Validation amountTextField.validationResultProperty().bind(model.amountValidationResult); @@ -583,6 +602,7 @@ private void addBindings() { paymentTitledGroupBg.managedProperty().bind(paymentTitledGroupBg.visibleProperty()); currencyComboBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); currencyComboBox.managedProperty().bind(currencyComboBox.visibleProperty()); + roundToComboBox.managedProperty().bind(roundToComboBox.visibleProperty()); currencyTextFieldBox.managedProperty().bind(currencyTextFieldBox.visibleProperty()); } @@ -610,6 +630,7 @@ private void removeBindings() { tradeFeeDescriptionLabel.textProperty().unbind(); tradeFeeInXmrLabel.visibleProperty().unbind(); tradeFeeDescriptionLabel.visibleProperty().unbind(); + roundToComboBox.valueProperty().unbindBidirectional(model.roundTo); // Validation amountTextField.validationResultProperty().unbind(); @@ -633,6 +654,7 @@ private void removeBindings() { paymentAccountsComboBox.managedProperty().unbind(); currencyComboBox.managedProperty().unbind(); currencyComboBox.prefWidthProperty().unbind(); + roundToComboBox.managedProperty().unbind(); currencyTextFieldBox.managedProperty().unbind(); } @@ -689,6 +711,10 @@ private void createListeners() { triggerPriceInputTextField.setText(model.triggerPrice.get()); }; + roundToSelectionFocusedListener = (o, oldValue, newValue) -> { + model.onFocusOutRoundToSelection(oldValue, newValue); + }; + errorMessageListener = (o, oldValue, newValue) -> { if (newValue != null) UserThread.runAfter(() -> new Popup().error(Res.get("createOffer.amountPriceBox.error.message", model.errorMessage.get())) @@ -697,6 +723,7 @@ private void createListeners() { paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected(); currencyComboBoxSelectionHandler = e -> onCurrencyComboBoxSelected(); + roundToComboBoxSelectionHandler = e -> onRoundToComboBoxSelected(); tradeCurrencyCodeListener = (observable, oldValue, newValue) -> { fixedPriceTextField.clear(); @@ -865,6 +892,7 @@ private void addListeners() { marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener); buyerSecurityDepositInputTextField.focusedProperty().addListener(buyerSecurityDepositFocusedListener); + roundToComboBox.focusedProperty().addListener(roundToSelectionFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); @@ -878,6 +906,7 @@ private void addListeners() { // UI actions paymentAccountsComboBox.setOnAction(paymentAccountsComboBoxSelectionHandler); currencyComboBox.setOnAction(currencyComboBoxSelectionHandler); + roundToComboBox.setOnAction(roundToComboBoxSelectionHandler); } private void removeListeners() { @@ -897,6 +926,7 @@ private void removeListeners() { marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener); buyerSecurityDepositInputTextField.focusedProperty().removeListener(buyerSecurityDepositFocusedListener); + roundToComboBox.focusedProperty().removeListener(roundToSelectionFocusedListener); // notifications model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); @@ -910,6 +940,7 @@ private void removeListeners() { // UI actions paymentAccountsComboBox.setOnAction(null); currencyComboBox.setOnAction(null); + roundToComboBox.setOnAction(null); } @@ -945,12 +976,14 @@ private void addPaymentGroup() { Res.get("shared.chooseTradingAccount"), Res.get("shared.chooseTradingAccount")); final Tuple3> currencyBoxTuple = addTopLabelComboBox( Res.get("shared.currency"), Res.get("list.currency.select")); + final Tuple3> roundingBoxTuple = addTopLabelComboBox( + Res.get("shared.roundTo"), Res.get("shared.roundTo")); currencySelection = currencyBoxTuple.first; paymentGroupBox.getChildren().addAll(tradingAccountBoxTuple.first, currencySelection); GridPane.setRowIndex(paymentGroupBox, gridRow); - GridPane.setColumnSpan(paymentGroupBox, 2); + GridPane.setColumnSpan(paymentGroupBox, 3); GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(paymentGroupBox); @@ -960,6 +993,13 @@ private void addPaymentGroup() { paymentAccountsComboBox.setPrefWidth(tradingAccountBoxTuple.first.getMinWidth()); editOfferElements.add(tradingAccountBoxTuple.first); + roundingBoxTuple.first.setMinWidth(75); + roundToComboBox = roundingBoxTuple.third; + roundToComboBox.setMinWidth(roundingBoxTuple.first.getMinWidth()); + roundToComboBox.setPrefWidth(roundingBoxTuple.first.getMinWidth()); + editOfferElements.add(roundingBoxTuple.first); + paymentGroupBox.getChildren().add(roundingBoxTuple.first); + // we display either currencyComboBox (multi currency account) or currencyTextField (single) currencyComboBox = currencyBoxTuple.third; currencyComboBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2); @@ -1449,6 +1489,10 @@ private ObservableList getPaymentAccounts() { return filterPaymentAccounts(model.getDataModel().getPaymentAccounts()); } + private ObservableList getRoundingList() { + return model.getDataModel().getRoundingList(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Abstract Methods /////////////////////////////////////////////////////////////////////////////////////////// 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 fa0dfbbf72b..09f9a1025a8 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -113,6 +113,7 @@ public abstract class MutableOfferViewModel ext public final StringProperty amount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty(); protected final StringProperty buyerSecurityDeposit = new SimpleStringProperty(); + protected final ObjectProperty roundTo = new SimpleObjectProperty<>(); final StringProperty buyerSecurityDepositInBTC = new SimpleStringProperty(); final StringProperty buyerSecurityDepositLabel = new SimpleStringProperty(); @@ -164,12 +165,14 @@ public abstract class MutableOfferViewModel ext private ChangeListener priceStringListener, marketPriceMarginStringListener; private ChangeListener volumeStringListener; private ChangeListener securityDepositStringListener; + private ChangeListener roundToNumberListener; private ChangeListener amountListener; private ChangeListener minAmountListener; private ChangeListener priceListener; private ChangeListener volumeListener; private ChangeListener securityDepositAsDoubleListener; + private ChangeListener roundToListener; private ChangeListener isWalletFundedListener; private ChangeListener errorMessageListener; @@ -231,6 +234,7 @@ public void activate() { setAmountToModel(); setMinAmountToModel(); setPriceToModel(); + setRoundToToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); updateButtonDisableState(); @@ -344,6 +348,7 @@ private void createListeners() { } } } + dataModel.onRoundingSelected(dataModel.getRoundToSelection().get()); updateButtonDisableState(); }; marketPriceMarginStringListener = (ov, oldValue, newValue) -> { @@ -428,6 +433,10 @@ private void createListeners() { } }; + roundToNumberListener = (ov, oldValue, newValue) -> { + roundTo.set((Integer) newValue); + updateButtonDisableState(); + }; amountListener = (ov, oldValue, newValue) -> { if (newValue != null) { @@ -466,6 +475,12 @@ private void createListeners() { ignoreVolumeStringListener = false; applyMakerFee(); }; + roundToListener = (ov, oldValue, newValue) -> { + if (newValue != null) + roundTo.set((Integer) newValue); + else + roundTo.set(1); + }; securityDepositAsDoubleListener = (ov, oldValue, newValue) -> { if (newValue != null) { @@ -525,6 +540,7 @@ private void addListeners() { dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); buyerSecurityDeposit.addListener(securityDepositStringListener); + roundTo.addListener(roundToNumberListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().addListener(amountListener); @@ -532,6 +548,7 @@ private void addListeners() { dataModel.getPrice().addListener(priceListener); dataModel.getVolume().addListener(volumeListener); dataModel.getBuyerSecurityDepositPct().addListener(securityDepositAsDoubleListener); + dataModel.getRoundToSelection().addListener(roundToListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().addListener(isWalletFundedListener); @@ -547,6 +564,7 @@ private void removeListeners() { dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); buyerSecurityDeposit.removeListener(securityDepositStringListener); + roundTo.removeListener(roundToNumberListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.getAmount().removeListener(amountListener); @@ -554,6 +572,7 @@ private void removeListeners() { dataModel.getPrice().removeListener(priceListener); dataModel.getVolume().removeListener(volumeListener); dataModel.getBuyerSecurityDepositPct().removeListener(securityDepositAsDoubleListener); + dataModel.getRoundToSelection().removeListener(roundToListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -672,6 +691,32 @@ public void onCurrencySelected(TradeCurrency tradeCurrency) { marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); updateButtonDisableState(); } + public void onEditOffer() { + final String currencyCode = dataModel.getTradeCurrencyCode().get(); + try { + double volumeAsDouble = ParsingUtils.parseNumberStringToDouble(volume.get()); + double amountAsDouble = ParsingUtils.parseNumberStringToDouble(amount.get()); + double manualPriceAsDouble = offerUtil.calculateManualPrice(volumeAsDouble, amountAsDouble); + int precision = CurrencyUtil.isTraditionalCurrency(currencyCode) ? + TraditionalMoney.SMALLEST_UNIT_EXPONENT : CryptoMoney.SMALLEST_UNIT_EXPONENT; + price.set(FormattingUtils.formatRoundedDoubleWithPrecision(manualPriceAsDouble, precision)); + priceStringListener.changed(null,null,price.get()); + setPriceToModel(); + } catch (NumberFormatException t) { + price.set(""); + new Popup().warning(Res.get("validation.NaN")).show(); + } + } + public void onRoundingSelected(Integer rounding) { + dataModel.onRoundingSelected(rounding); + + if (isVolumeInputValid(volume.get()).isValid) { + setVolumeToModel(); + setPriceToModel(); + dataModel.calculateAmount(); + dataModel.calculateTotalToPay(); + } + } void onShowPayFundsScreen(Runnable actionHandler) { actionHandler.run(); @@ -806,6 +851,13 @@ void onFocusOutTriggerPriceTextField(boolean oldValue, boolean newValue) { } } + void onFocusOutRoundToSelection(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + this.roundTo.set(this.roundTo.get()); // Do nothing + setRoundToToModel(); + } + } + public void onTriggerPriceTextFieldChanged() { String triggerPriceAsString = triggerPrice.get(); @@ -1178,6 +1230,18 @@ private void setPriceToModel() { } } + private void setRoundToToModel() { + if (roundTo.get() != null) { + try { + dataModel.setRoundToSelection(this.roundTo.get()); + } catch (Throwable t) { + log.debug(t.getMessage()); + } + } else { + dataModel.setRoundToSelection(1); + } + } + private void setVolumeToModel() { if (volume.get() != null && !volume.get().isEmpty()) { try { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 75514e43002..0f72c4f9923 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -84,6 +84,7 @@ public void populateData(Offer offer) { setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); + setRoundToSelection(offer.getRoundToSelection()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); setBuyerSecurityDeposit(getBuyerSecurityAsPercent(offer)); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index be2b811f07a..e8cf1eebb2a 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -92,6 +92,7 @@ public void reset() { useMarketBasedPrice.set(false); amount.set(null); minAmount.set(null); + roundTo.set(null); price.set(null); volume.set(null); minVolume.set(null); @@ -121,7 +122,7 @@ public void applyOpenOffer(OpenOffer openOffer) { else paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); } - + // TODO: update for XMR to use percent as double? // If the security deposit got bounded because it was below the coin amount limit, it can be bigger @@ -161,6 +162,7 @@ public void populateData() { setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); + setRoundToSelection(offer.getRoundToSelection()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); setTriggerPrice(openOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { @@ -189,6 +191,7 @@ public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler erro newOfferPayload.isUseMarketBasedPrice(), offerPayload.getAmount(), offerPayload.getMinAmount(), + offerPayload.getRoundTo(), offerPayload.getMakerFeePct(), offerPayload.getTakerFeePct(), offerPayload.getPenaltyFeePct(), diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java index 60aa3c4dec2..8280ccc12c8 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java @@ -98,6 +98,7 @@ protected void doActivate() { minAmountBtcLabel.setDisable(true); volumeTextField.setDisable(true); volumeCurrencyLabel.setDisable(true); + roundToComboBox.setDisable(true); // Workaround to fix margin on top of amount group gridPane.setPadding(new Insets(-20, 25, -1, 25)); @@ -201,6 +202,7 @@ private void addConfirmEditGroup() { confirmButton.setOnAction(e -> { if (model.isPriceInRange()) { model.isNextButtonDisabled.setValue(true); + model.onEditOffer(); cancelButton.setDisable(true); busyAnimation.play(); spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); diff --git a/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java index 19c23592454..cd3174d2143 100644 --- a/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -67,6 +67,7 @@ public class TradesChartsViewModelTest { false, 0, 0, + 1, 0, 0, 0, diff --git a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java index bce59e6541f..54521a84772 100644 --- a/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/haveno/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -605,6 +605,7 @@ private Offer getOffer(String tradeCurrencyCode, false, 0, 0, + 1, 0, 0, 0, diff --git a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java index ae6f73ac523..6ddf6dcbc41 100644 --- a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java @@ -49,6 +49,7 @@ public class OfferMaker { public static final Property date = newProperty(); public static final Property price = newProperty(); public static final Property minAmount = newProperty(); + public static final Property roundTo = newProperty(); public static final Property amount = newProperty(); public static final Property baseCurrencyCode = newProperty(); public static final Property counterCurrencyCode = newProperty(); @@ -82,6 +83,7 @@ public class OfferMaker { lookup.valueOf(useMarketBasedPrice, false), lookup.valueOf(amount, 100000L), lookup.valueOf(minAmount, 100000L), + lookup.valueOf(roundTo, 1), lookup.valueOf(makerFeePct, .0015), lookup.valueOf(takerFeePct, .0075), lookup.valueOf(penaltyFeePct, 0.03), diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index c3dcf498336..5db885eba23 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -513,10 +513,11 @@ message PostOfferRequest { double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; - double buyer_security_deposit_pct = 8; - string trigger_price = 9; - bool reserve_exact_amount = 10; - string payment_account_id = 11; + int32 round_to = 8; + double buyer_security_deposit_pct = 9; + string trigger_price = 10; + bool reserve_exact_amount = 11; + string payment_account_id = 12; } message PostOfferReply { @@ -538,30 +539,31 @@ message OfferInfo { double market_price_margin_pct = 5; uint64 amount = 6 [jstype = JS_STRING]; uint64 min_amount = 7 [jstype = JS_STRING]; - double maker_fee_pct = 8; - double taker_fee_pct = 9; - double penalty_fee_pct = 10; - double buyer_security_deposit_pct = 11; - double seller_security_deposit_pct = 12; - string volume = 13; - string min_volume = 14; - string trigger_price = 15; - string payment_account_id = 16; - string payment_method_id = 17; - string payment_method_short_name = 18; - string base_currency_code = 19; - string counter_currency_code = 20; - uint64 date = 21; - string state = 22; - bool is_activated = 23; - bool is_my_offer = 24; - string owner_node_address = 25; - string pub_key_ring = 26; - string version_nr = 27; - int32 protocol_version = 28; - string arbitrator_signer = 29; - string split_output_tx_hash = 30; - uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; + int32 roundTo = 8; + double maker_fee_pct = 9; + double taker_fee_pct = 10; + double penalty_fee_pct = 11; + double buyer_security_deposit_pct = 12; + double seller_security_deposit_pct = 13; + string volume = 14; + string min_volume = 15; + string trigger_price = 16; + string payment_account_id = 17; + string payment_method_id = 18; + string payment_method_short_name = 19; + string base_currency_code = 20; + string counter_currency_code = 21; + uint64 date = 22; + string state = 23; + bool is_activated = 24; + bool is_my_offer = 25; + string owner_node_address = 26; + string pub_key_ring = 27; + string version_nr = 28; + int32 protocol_version = 29; + string arbitrator_signer = 30; + string split_output_tx_hash = 31; + uint64 split_output_tx_fee = 32 [jstype = JS_STRING]; } message AvailabilityResultWithDescription { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 8e988bbe571..1410d0ee4f7 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -628,34 +628,35 @@ message OfferPayload { bool use_market_based_price = 8; int64 amount = 9; int64 min_amount = 10; - double maker_fee_pct = 11; - double taker_fee_pct = 12; - double penalty_fee_pct = 13; - double buyer_security_deposit_pct = 14; - double seller_security_deposit_pct = 15; - string base_currency_code = 16; - string counter_currency_code = 17; - string payment_method_id = 18; - string maker_payment_account_id = 19; - string country_code = 20; - repeated string accepted_country_codes = 21; - string bank_id = 22; - repeated string accepted_bank_ids = 23; - string version_nr = 24; - int64 block_height_at_offer_creation = 25; - int64 max_trade_limit = 26; - int64 max_trade_period = 27; - bool use_auto_close = 28; - bool use_re_open_after_auto_close = 29; - int64 lower_close_price = 30; - int64 upper_close_price = 31; - bool is_private_offer = 32; - string hash_of_challenge = 33; - map extra_data = 34; - int32 protocol_version = 35; - NodeAddress arbitrator_signer = 36; - bytes arbitrator_signature = 37; - repeated string reserve_tx_key_images = 38; + int32 round_to = 11; + double maker_fee_pct = 12; + double taker_fee_pct = 13; + double penalty_fee_pct = 14; + double buyer_security_deposit_pct = 15; + double seller_security_deposit_pct = 16; + string base_currency_code = 17; + string counter_currency_code = 18; + string payment_method_id = 19; + string maker_payment_account_id = 20; + string country_code = 21; + repeated string accepted_country_codes = 22; + string bank_id = 23; + repeated string accepted_bank_ids = 24; + string version_nr = 25; + int64 block_height_at_offer_creation = 26; + int64 max_trade_limit = 27; + int64 max_trade_period = 28; + bool use_auto_close = 29; + bool use_re_open_after_auto_close = 30; + int64 lower_close_price = 31; + int64 upper_close_price = 32; + bool is_private_offer = 33; + string hash_of_challenge = 34; + map extra_data = 35; + int32 protocol_version = 36; + NodeAddress arbitrator_signer = 37; + bytes arbitrator_signature = 38; + repeated string reserve_tx_key_images = 39; } enum OfferDirection {