diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index b4a293a3b91..b2d4246466e 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -420,7 +420,8 @@ public void postOffer(String currencyCode, boolean reserveExactAmount, String paymentAccountId, Consumer resultHandler, - ErrorMessageHandler errorMessageHandler) { + ErrorMessageHandler errorMessageHandler, + int roundTo) { coreOffersService.postOffer(currencyCode, directionAsString, priceAsString, @@ -433,7 +434,8 @@ public void postOffer(String currencyCode, reserveExactAmount, paymentAccountId, resultHandler, - errorMessageHandler); + errorMessageHandler, + roundTo); } public Offer editOffer(String offerId, @@ -445,7 +447,8 @@ public Offer editOffer(String offerId, BigInteger amount, BigInteger minAmount, double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + PaymentAccount paymentAccount, + Integer roundTo) { return coreOffersService.editOffer(offerId, currencyCode, direction, @@ -455,7 +458,8 @@ public Offer editOffer(String offerId, amount, minAmount, buyerSecurityDeposit, - paymentAccount); + paymentAccount, + roundTo); } public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index 7e67965c9ce..bcbbbb28f3c 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -177,7 +177,8 @@ void postOffer(String currencyCode, boolean reserveExactAmount, String paymentAccountId, Consumer resultHandler, - ErrorMessageHandler errorMessageHandler) { + ErrorMessageHandler errorMessageHandler, + int roundTo) { coreWalletsService.verifyWalletsAreAvailable(); coreWalletsService.verifyEncryptedWalletIsUnlocked(); @@ -200,7 +201,8 @@ void postOffer(String currencyCode, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), securityDeposit, - paymentAccount); + paymentAccount, + roundTo); verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount); @@ -224,7 +226,8 @@ Offer editOffer(String offerId, BigInteger amount, BigInteger minAmount, double buyerSecurityDeposit, - PaymentAccount paymentAccount) { + PaymentAccount paymentAccount, + Integer roundTo) { return createOfferService.createAndGetOffer(offerId, direction, currencyCode.toUpperCase(), @@ -234,7 +237,8 @@ Offer editOffer(String offerId, useMarketBasedPrice, exactMultiply(marketPriceMargin, 0.01), buyerSecurityDeposit, - paymentAccount); + paymentAccount, + roundTo); } void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { 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..f668b91a200 100644 --- a/core/src/main/java/haveno/core/api/model/OfferInfo.java +++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java @@ -78,6 +78,7 @@ public class OfferInfo implements Payload { @Nullable private final String splitOutputTxHash; private final long splitOutputTxFee; + private final int roundTo; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.getId(); @@ -111,6 +112,7 @@ public OfferInfo(OfferInfoBuilder builder) { this.arbitratorSigner = builder.getArbitratorSigner(); this.splitOutputTxHash = builder.getSplitOutputTxHash(); this.splitOutputTxFee = builder.getSplitOutputTxFee(); + this.roundTo = builder.getRoundTo(); } public static OfferInfo toOfferInfo(Offer offer) { @@ -177,7 +179,8 @@ private static OfferInfoBuilder getBuilder(Offer offer) { .withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString()) .withVersionNumber(offer.getOfferPayload().getVersionNr()) .withProtocolVersion(offer.getOfferPayload().getProtocolVersion()) - .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()); + .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress()) + .withRoundTo(offer.getRoundTo()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -215,7 +218,8 @@ public haveno.proto.grpc.OfferInfo toProtoMessage() { .setPubKeyRing(pubKeyRing) .setVersionNr(versionNumber) .setProtocolVersion(protocolVersion) - .setSplitOutputTxFee(splitOutputTxFee); + .setSplitOutputTxFee(splitOutputTxFee) + .setRoundTo(roundTo); Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner); Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash); return builder.build(); @@ -255,6 +259,7 @@ public static OfferInfo fromProto(haveno.proto.grpc.OfferInfo proto) { .withArbitratorSigner(proto.getArbitratorSigner()) .withSplitOutputTxHash(proto.getSplitOutputTxHash()) .withSplitOutputTxFee(proto.getSplitOutputTxFee()) + .withRoundTo(proto.getRoundTo()) .build(); } } 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..49165311f14 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 @@ -63,6 +63,7 @@ public final class OfferInfoBuilder { private String arbitratorSigner; private String splitOutputTxHash; private long splitOutputTxFee; + private int roundTo; public OfferInfoBuilder withId(String id) { this.id = id; @@ -223,7 +224,7 @@ public OfferInfoBuilder withArbitratorSigner(String arbitratorSigner) { this.arbitratorSigner = arbitratorSigner; return this; } - + public OfferInfoBuilder withSplitOutputTxHash(String splitOutputTxHash) { this.splitOutputTxHash = splitOutputTxHash; return this; @@ -234,6 +235,11 @@ public OfferInfoBuilder withSplitOutputTxFee(long splitOutputTxFee) { return this; } + public OfferInfoBuilder withRoundTo(int roundTo) { + this.roundTo = roundTo; + return this; + } + public OfferInfo build() { return new OfferInfo(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..ec5686d5c02 100644 --- a/core/src/main/java/haveno/core/offer/CreateOfferService.java +++ b/core/src/main/java/haveno/core/offer/CreateOfferService.java @@ -103,12 +103,14 @@ public Offer createAndGetOffer(String offerId, boolean useMarketBasedPrice, double marketPriceMargin, double securityDepositAsDouble, - PaymentAccount paymentAccount) { + PaymentAccount paymentAccount, + Integer roundTo) { log.info("create and get offer with 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, @@ -216,7 +219,8 @@ public Offer createAndGetOffer(String offerId, Version.TRADE_PROTOCOL_VERSION, null, null, - null); + null, + roundTo); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); return offer; diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 5c2f6eb51b4..5d64ed5cf25 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 getRoundTo() { + 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..34dc2b23c95 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -338,7 +338,8 @@ private void doDumpStatistics() { offer.getId(), offer.isUseMarketBasedPrice(), offer.getMarketPriceMarginPct(), - offer.getPaymentMethod() + offer.getPaymentMethod(), + offer.getRoundTo() ); } catch (Throwable t) { // In case an offer was corrupted with null values we ignore it diff --git a/core/src/main/java/haveno/core/offer/OfferForJson.java b/core/src/main/java/haveno/core/offer/OfferForJson.java index caebcdc3dd1..db3b905fde6 100644 --- a/core/src/main/java/haveno/core/offer/OfferForJson.java +++ b/core/src/main/java/haveno/core/offer/OfferForJson.java @@ -26,6 +26,7 @@ import haveno.core.monetary.Volume; import haveno.core.payment.payload.PaymentMethod; import haveno.core.trade.HavenoUtils; +import lombok.Getter; import org.bitcoinj.utils.MonetaryFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +48,8 @@ public class OfferForJson { public final double marketPriceMargin; public final String paymentMethod; public final String id; + @Getter + public final int roundTo; // primaryMarket fields are based on industry standard where primaryMarket is always in the focus (in the app BTC is always in the focus - will be changed in a larger refactoring once) public String currencyPair; @@ -81,7 +84,8 @@ public OfferForJson(OfferDirection direction, String id, boolean useMarketBasedPrice, double marketPriceMargin, - PaymentMethod paymentMethod) { + PaymentMethod paymentMethod, + Integer roundTo) { this.direction = direction; this.currencyCode = currencyCode; @@ -93,6 +97,7 @@ public OfferForJson(OfferDirection direction, this.useMarketBasedPrice = useMarketBasedPrice; this.marketPriceMargin = marketPriceMargin; this.paymentMethod = paymentMethod.getId(); + this.roundTo = roundTo; setDisplayStrings(); } diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 1a194a1a98b..e2a309d0d0f 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -90,6 +90,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay @Setter @Nullable protected List reserveTxKeyImages; + protected final int roundTo; // Keys for extra map // Only set for traditional offers @@ -198,7 +199,8 @@ public OfferPayload(String id, int protocolVersion, @Nullable NodeAddress arbitratorSigner, @Nullable byte[] arbitratorSignature, - @Nullable List reserveTxKeyImages) { + @Nullable List reserveTxKeyImages, + int roundTo) { this.id = id; this.date = date; this.ownerNodeAddress = ownerNodeAddress; @@ -237,6 +239,7 @@ public OfferPayload(String id, this.upperClosePrice = upperClosePrice; this.isPrivateOffer = isPrivateOffer; this.hashOfChallenge = hashOfChallenge; + this.roundTo = roundTo; } public byte[] getHash() { @@ -287,7 +290,8 @@ public byte[] getSignatureHash() { protocolVersion, arbitratorSigner, null, - reserveTxKeyImages + reserveTxKeyImages, + roundTo ); return signee.getHash(); @@ -368,7 +372,8 @@ public protobuf.StoragePayload toProtoMessage() { .setLowerClosePrice(lowerClosePrice) .setUpperClosePrice(upperClosePrice) .setIsPrivateOffer(isPrivateOffer) - .setProtocolVersion(protocolVersion); + .setProtocolVersion(protocolVersion) + .setRoundTo(roundTo); Optional.ofNullable(ownerNodeAddress).ifPresent(e -> builder.setOwnerNodeAddress(ownerNodeAddress.toProtoMessage())); Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode); Optional.ofNullable(bankId).ifPresent(builder::setBankId); @@ -429,7 +434,8 @@ public static OfferPayload fromProto(protobuf.OfferPayload proto) { proto.getProtocolVersion(), proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null, ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()), - proto.getReserveTxKeyImagesList() == null ? null : new ArrayList(proto.getReserveTxKeyImagesList())); + proto.getReserveTxKeyImagesList() == null ? null : new ArrayList(proto.getReserveTxKeyImagesList()), + proto.getRoundTo()); } @Override @@ -474,6 +480,7 @@ public String toString() { ",\r\n hashOfChallenge='" + hashOfChallenge + '\'' + ",\r\n arbitratorSigner=" + arbitratorSigner + ",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + + ",\r\n roundTo=" + roundTo + "\r\n} "; } @@ -515,6 +522,7 @@ public JsonElement serialize(OfferPayload offerPayload, Type type, JsonSerializa object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion())); object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner())); object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature())); + object.add("roundTo", context.serialize(offerPayload.getRoundTo())); return object; } } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index f507cd9758d..3901437c3c1 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); @@ -1642,7 +1642,8 @@ private void maybeUpdatePersistedOffers() { protocolVersion, originalOfferPayload.getArbitratorSigner(), originalOfferPayload.getArbitratorSignature(), - originalOfferPayload.getReserveTxKeyImages()); + originalOfferPayload.getReserveTxKeyImages(), + originalOfferPayload.getRoundTo()); // Save states from original data to use for the updated Offer.State originalOfferState = originalOffer.getState(); 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 btcUsdOffer = a(Offer); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 04b294e4518..c3245b9b645 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -167,7 +167,8 @@ public void postOffer(PostOfferRequest req, }, errorMessage -> { if (!errorMessageHandler.isErrorHandled()) errorMessageHandler.handleErrorMessage(errorMessage); - }); + }, + req.getRoundTo()); } catch (Throwable cause) { exceptionHandler.handleException(log, cause, responseObserver); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index f94ccbd3f70..0392ae3dae1 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -53,15 +53,18 @@ import haveno.desktop.util.GUIUtil; import haveno.network.p2p.P2PService; import java.math.BigInteger; -import java.util.Comparator; + import static java.util.Comparator.comparing; + +import java.util.Comparator; import java.util.Date; import java.util.HashSet; -import java.util.Objects; import java.util.Optional; +import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -104,6 +107,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel { protected final ObjectProperty 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(); @@ -288,7 +292,8 @@ protected Offer createAndGetOffer() { useMarketBasedPrice.get(), useMarketBasedPrice.get() ? marketPriceMargin : 0, buyerSecurityDepositPct.get(), - paymentAccount); + paymentAccount, + roundTo.get()); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -361,6 +366,15 @@ private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) { tradeCurrencyCode.set(tradeCurrency.getCode()); } + protected void onRoundToSelected(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 getRoundToList() { + // 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 getRoundTo() { + 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..aba17326f6c 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(getRoundToList()); 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.onRoundToSelected(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 getRoundToList() { + return model.getDataModel().getRoundToList(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // 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..a65a7b328dc 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.onRoundToSelected(dataModel.getRoundTo().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.getRoundTo().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.getRoundTo().removeListener(roundToListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -672,6 +691,31 @@ public void onCurrencySelected(TradeCurrency tradeCurrency) { marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); updateButtonDisableState(); } + public void onEditOfferConfirmed() { + 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)); + setPriceToModel(); + } catch (NumberFormatException t) { + price.set(""); + new Popup().warning(Res.get("validation.NaN")).show(); + } + } + public void onRoundToSelected(Integer rounding) { + dataModel.onRoundToSelected(rounding); + + if (isVolumeInputValid(volume.get()).isValid) { + setVolumeToModel(); + setPriceToModel(); + dataModel.calculateAmount(); + dataModel.calculateTotalToPay(); + } + } void onShowPayFundsScreen(Runnable actionHandler) { actionHandler.run(); @@ -806,6 +850,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 +1229,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..6ba02b6f5f1 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.getRoundTo()); 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..85b9449745c 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 @@ -99,6 +99,7 @@ public void reset() { paymentAccounts.clear(); paymentAccount = null; marketPriceMargin = 0; + roundTo.set(null); } public void applyOpenOffer(OpenOffer openOffer) { @@ -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.getRoundTo()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); setTriggerPrice(openOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { @@ -216,7 +218,8 @@ public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler erro offerPayload.getProtocolVersion(), offerPayload.getArbitratorSigner(), offerPayload.getArbitratorSignature(), - offerPayload.getReserveTxKeyImages()); + offerPayload.getReserveTxKeyImages(), + offerPayload.getRoundTo()); final Offer editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); 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..b457ffd2175 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.onEditOfferConfirmed(); 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..12f0f23844f 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 @@ -94,7 +94,8 @@ public class TradesChartsViewModelTest { 0, null, null, - null); + null, + 1); @BeforeEach public void setup() throws IOException { 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..eb78274e21a 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 @@ -632,7 +632,8 @@ private Offer getOffer(String tradeCurrencyCode, 0, null, null, - null)); + null, + 1)); } } diff --git a/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java b/desktop/src/test/java/haveno/desktop/maker/OfferMaker.java index ae6f73ac523..6450082b35b 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(); @@ -111,7 +112,8 @@ public class OfferMaker { lookup.valueOf(protocolVersion, 0), getLocalHostNodeWithPort(99999), null, - null)); + null, + lookup.valueOf(roundTo, 1))); public static final Maker xmrUsdOffer = a(Offer); public static final Maker btcBCHCOffer = a(Offer).but(with(counterCurrencyCode, "BCHC")); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index c3dcf498336..7d06922d307 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -517,6 +517,7 @@ message PostOfferRequest { string trigger_price = 9; bool reserve_exact_amount = 10; string payment_account_id = 11; + int32 round_to = 12; } message PostOfferReply { @@ -562,6 +563,7 @@ message OfferInfo { string arbitrator_signer = 29; string split_output_tx_hash = 30; uint64 split_output_tx_fee = 31 [jstype = JS_STRING]; + int32 round_to = 32; } message AvailabilityResultWithDescription { diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 8e988bbe571..92d09ff364d 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -656,6 +656,7 @@ message OfferPayload { NodeAddress arbitrator_signer = 36; bytes arbitrator_signature = 37; repeated string reserve_tx_key_images = 38; + int32 round_to = 39; } enum OfferDirection {