From 81da6fbe5aed21446aa4106cecba9db28d04a819 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 12 Jun 2021 18:35:27 -0300 Subject: [PATCH 01/40] Refactor EditOfferDataModel for new editoffer api method - Define set of editable offer payload fields in MutableOfferPayloadFields. - Move bulk of EditOfferDataModel#onPublishOffer logic to OfferUtil. --- .../core/offer/MutableOfferPayloadFields.java | 89 +++++++++++++++++++ .../main/java/bisq/core/offer/OfferUtil.java | 47 ++++++++++ .../editoffer/EditOfferDataModel.java | 49 +--------- 3 files changed, 140 insertions(+), 45 deletions(-) create mode 100644 core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java diff --git a/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java b/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java new file mode 100644 index 00000000000..56700fd63aa --- /dev/null +++ b/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java @@ -0,0 +1,89 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.offer; + +import java.util.List; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.annotation.Nullable; + +/** + * The set of editable OfferPayload fields. + */ +@Getter +@Setter +@ToString +public final class MutableOfferPayloadFields { + + private final long price; + private final double marketPriceMargin; + private final boolean useMarketBasedPrice; + private final String baseCurrencyCode; + private final String counterCurrencyCode; + private final String paymentMethodId; + private final String makerPaymentAccountId; + @Nullable + private final String countryCode; + @Nullable + private final List acceptedCountryCodes; + @Nullable + private final String bankId; + @Nullable + private final List acceptedBankIds; + + public MutableOfferPayloadFields(OfferPayload offerPayload) { + this(offerPayload.getPrice(), + offerPayload.getMarketPriceMargin(), + offerPayload.isUseMarketBasedPrice(), + offerPayload.getBaseCurrencyCode(), + offerPayload.getCounterCurrencyCode(), + offerPayload.getPaymentMethodId(), + offerPayload.getMakerPaymentAccountId(), + offerPayload.getCountryCode(), + offerPayload.getAcceptedCountryCodes(), + offerPayload.getBankId(), + offerPayload.getAcceptedBankIds()); + } + + public MutableOfferPayloadFields(long price, + double marketPriceMargin, + boolean useMarketBasedPrice, + String baseCurrencyCode, + String counterCurrencyCode, + String paymentMethodId, + String makerPaymentAccountId, + @Nullable String countryCode, + @Nullable List acceptedCountryCodes, + @Nullable String bankId, + @Nullable List acceptedBankIds) { + this.price = price; + this.marketPriceMargin = marketPriceMargin; + this.useMarketBasedPrice = useMarketBasedPrice; + this.baseCurrencyCode = baseCurrencyCode; + this.counterCurrencyCode = counterCurrencyCode; + this.paymentMethodId = paymentMethodId; + this.makerPaymentAccountId = makerPaymentAccountId; + this.countryCode = countryCode; + this.acceptedCountryCodes = acceptedCountryCodes; + this.bankId = bankId; + this.acceptedBankIds = acceptedBankIds; + } +} diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 8ea47fd2be0..f67a73d286f 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -364,6 +364,53 @@ public void validateOfferData(double buyerSecurityDeposit, Res.get("offerbook.warning.paymentMethodBanned")); } + // Returns an edited payload: a merge of the original offerPayload and + // editedOfferPayload fields. Mutable fields are sourced from + // mutableOfferPayloadFields param, e.g., payment account details, price, etc. + // Immutable fields are sourced from the original openOffer payload param. + public OfferPayload getMergedOfferPayload(OpenOffer openOffer, + MutableOfferPayloadFields mutableOfferPayloadFields) { + OfferPayload originalOfferPayload = openOffer.getOffer().getOfferPayload(); + return new OfferPayload(originalOfferPayload.getId(), + originalOfferPayload.getDate(), + originalOfferPayload.getOwnerNodeAddress(), + originalOfferPayload.getPubKeyRing(), + originalOfferPayload.getDirection(), + mutableOfferPayloadFields.getPrice(), + mutableOfferPayloadFields.getMarketPriceMargin(), + mutableOfferPayloadFields.isUseMarketBasedPrice(), + originalOfferPayload.getAmount(), + originalOfferPayload.getMinAmount(), + mutableOfferPayloadFields.getBaseCurrencyCode(), + mutableOfferPayloadFields.getCounterCurrencyCode(), + originalOfferPayload.getArbitratorNodeAddresses(), + originalOfferPayload.getMediatorNodeAddresses(), + mutableOfferPayloadFields.getPaymentMethodId(), + mutableOfferPayloadFields.getMakerPaymentAccountId(), + originalOfferPayload.getOfferFeePaymentTxId(), + mutableOfferPayloadFields.getCountryCode(), + mutableOfferPayloadFields.getAcceptedCountryCodes(), + mutableOfferPayloadFields.getBankId(), + mutableOfferPayloadFields.getAcceptedBankIds(), + originalOfferPayload.getVersionNr(), + originalOfferPayload.getBlockHeightAtOfferCreation(), + originalOfferPayload.getTxFee(), + originalOfferPayload.getMakerFee(), + originalOfferPayload.isCurrencyForMakerFeeBtc(), + originalOfferPayload.getBuyerSecurityDeposit(), + originalOfferPayload.getSellerSecurityDeposit(), + originalOfferPayload.getMaxTradeLimit(), + originalOfferPayload.getMaxTradePeriod(), + originalOfferPayload.isUseAutoClose(), + originalOfferPayload.isUseReOpenAfterAutoClose(), + originalOfferPayload.getLowerClosePrice(), + originalOfferPayload.getUpperClosePrice(), + originalOfferPayload.isPrivateOffer(), + originalOfferPayload.getHashOfChallenge(), + originalOfferPayload.getExtraDataMap(), + originalOfferPayload.getProtocolVersion()); + } + private Optional getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc, String userCurrencyCode, diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index dc7d29c7c08..abf533c4723 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -28,6 +28,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.TradeCurrency; import bisq.core.offer.CreateOfferService; +import bisq.core.offer.MutableOfferPayloadFields; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; @@ -182,54 +183,12 @@ public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { } public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - // editedPayload is a merge of the original offerPayload and newOfferPayload - // fields which are editable are merged in from newOfferPayload (such as payment account details) - // fields which cannot change (most importantly BTC amount) are sourced from the original offerPayload - final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload(); - final OfferPayload newOfferPayload = createAndGetOffer().getOfferPayload(); - final OfferPayload editedPayload = new OfferPayload(offerPayload.getId(), - offerPayload.getDate(), - offerPayload.getOwnerNodeAddress(), - offerPayload.getPubKeyRing(), - offerPayload.getDirection(), - newOfferPayload.getPrice(), - newOfferPayload.getMarketPriceMargin(), - newOfferPayload.isUseMarketBasedPrice(), - offerPayload.getAmount(), - offerPayload.getMinAmount(), - newOfferPayload.getBaseCurrencyCode(), - newOfferPayload.getCounterCurrencyCode(), - offerPayload.getArbitratorNodeAddresses(), - offerPayload.getMediatorNodeAddresses(), - newOfferPayload.getPaymentMethodId(), - newOfferPayload.getMakerPaymentAccountId(), - offerPayload.getOfferFeePaymentTxId(), - newOfferPayload.getCountryCode(), - newOfferPayload.getAcceptedCountryCodes(), - newOfferPayload.getBankId(), - newOfferPayload.getAcceptedBankIds(), - offerPayload.getVersionNr(), - offerPayload.getBlockHeightAtOfferCreation(), - offerPayload.getTxFee(), - offerPayload.getMakerFee(), - offerPayload.isCurrencyForMakerFeeBtc(), - offerPayload.getBuyerSecurityDeposit(), - offerPayload.getSellerSecurityDeposit(), - offerPayload.getMaxTradeLimit(), - offerPayload.getMaxTradePeriod(), - offerPayload.isUseAutoClose(), - offerPayload.isUseReOpenAfterAutoClose(), - offerPayload.getLowerClosePrice(), - offerPayload.getUpperClosePrice(), - offerPayload.isPrivateOffer(), - offerPayload.getHashOfChallenge(), - offerPayload.getExtraDataMap(), - offerPayload.getProtocolVersion()); - + MutableOfferPayloadFields mutableOfferPayloadFields = + new MutableOfferPayloadFields(createAndGetOffer().getOfferPayload()); + final OfferPayload editedPayload = offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields); final Offer editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); editedOffer.setState(Offer.State.AVAILABLE); - openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { openOffer = null; resultHandler.handleResult(); From d9dd718b4c63489c373df12602b8b8b2f49e80ed Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 12 Jun 2021 18:42:14 -0300 Subject: [PATCH 02/40] Fix comment --- core/src/main/java/bisq/core/offer/OfferUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index f67a73d286f..91c894feb0e 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -367,7 +367,7 @@ public void validateOfferData(double buyerSecurityDeposit, // Returns an edited payload: a merge of the original offerPayload and // editedOfferPayload fields. Mutable fields are sourced from // mutableOfferPayloadFields param, e.g., payment account details, price, etc. - // Immutable fields are sourced from the original openOffer payload param. + // Immutable fields are sourced from the original openOffer param. public OfferPayload getMergedOfferPayload(OpenOffer openOffer, MutableOfferPayloadFields mutableOfferPayloadFields) { OfferPayload originalOfferPayload = openOffer.getOffer().getOfferPayload(); From 1daf4715f843acdd816c90ee9ae0bf5aaf630a93 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 11:59:58 -0300 Subject: [PATCH 03/40] Add OfferInfo field isActivated, rpc EditOffer to proto --- proto/src/main/proto/grpc.proto | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index b8d50bfe4f8..0fa653e4c24 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -72,6 +72,8 @@ service Offers { } rpc CreateOffer (CreateOfferRequest) returns (CreateOfferReply) { } + rpc EditOffer (EditOfferRequest) returns (EditOfferReply) { + } rpc CancelOffer (CancelOfferRequest) returns (CancelOfferReply) { } } @@ -128,6 +130,35 @@ message CreateOfferReply { OfferInfo offer = 1; } +message EditOfferRequest { + string id = 1; + string price = 2; + bool useMarketBasedPrice = 3; + double marketPriceMargin = 4; + uint64 triggerPrice = 5; + // Send a signed int, not a bool (with default=false). + // -1 = do not change activation state + // 0 = disable + // 1 = enable + sint32 enable = 6; + // The EditType constricts what offer details can be modified and simplifies param validation. + enum EditType { + ACTIVATION_STATE_ONLY = 0; + FIXED_PRICE_ONLY = 1; + FIXED_PRICE_AND_ACTIVATION_STATE = 2; + MKT_PRICE_MARGIN_ONLY = 3; + MKT_PRICE_MARGIN_AND_ACTIVATION_STATE = 4; + TRIGGER_PRICE_ONLY = 5; + TRIGGER_PRICE_AND_ACTIVATION_STATE = 6; + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE = 7; + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE = 8; + } + EditType editType = 7; +} + +message EditOfferReply { +} + message CancelOfferRequest { string id = 1; } @@ -159,6 +190,7 @@ message OfferInfo { string offerFeePaymentTxId = 21; uint64 txFee = 22; uint64 makerFee = 23; + bool isActivated = 24; } message AvailabilityResultWithDescription { From 2b8b53bba86579d71e184aac38531e15f462084b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 12:24:45 -0300 Subject: [PATCH 04/40] Add server/core editOffer, adjust getMyOffer(s) impls - Add editOffer to GrpcOffersService, CoreApi, CoreOffersService. - Set editOffer call rate meter to 1 / minute. - Use new EditOfferValidator to verify editOffer params OK. - Adust getMyOffer(s) rpc impl and OfferInfo model to use OpenOffer for accessing activation state and trigger price. --- core/src/main/java/bisq/core/api/CoreApi.java | 43 ++--- .../java/bisq/core/api/CoreOffersService.java | 156 ++++++++++++------ .../bisq/core/api/EditOfferValidator.java | 135 +++++++++++++++ .../java/bisq/core/api/model/OfferInfo.java | 22 ++- .../bisq/daemon/grpc/GrpcOffersService.java | 30 +++- 5 files changed, 302 insertions(+), 84 deletions(-) create mode 100644 core/src/main/java/bisq/core/api/EditOfferValidator.java diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index fe5bbceba58..1c223df2b7f 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -21,9 +21,7 @@ import bisq.core.api.model.BalancesInfo; import bisq.core.api.model.TxFeeRateInfo; import bisq.core.btc.wallet.TxBroadcaster; -import bisq.core.monetary.Price; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; @@ -36,7 +34,6 @@ import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import javax.inject.Inject; @@ -52,6 +49,8 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import static bisq.proto.grpc.EditOfferRequest.EditType; + /** * Provides high level interface to functionality of core Bisq features. * E.g. useful for different APIs to access data of different domains of Bisq. @@ -122,7 +121,7 @@ public Offer getOffer(String id) { return coreOffersService.getOffer(id); } - public Offer getMyOffer(String id) { + public OpenOffer getMyOffer(String id) { return coreOffersService.getMyOffer(id); } @@ -130,14 +129,10 @@ public List getOffers(String direction, String currencyCode) { return coreOffersService.getOffers(direction, currencyCode); } - public List getMyOffers(String direction, String currencyCode) { + public List getMyOffers(String direction, String currencyCode) { return coreOffersService.getMyOffers(direction, currencyCode); } - public OpenOffer getMyOpenOffer(String id) { - return coreOffersService.getMyOpenOffer(id); - } - public void createAnPlaceOffer(String currencyCode, String directionAsString, String priceAsString, @@ -164,26 +159,20 @@ public void createAnPlaceOffer(String currencyCode, resultHandler); } - public Offer editOffer(String offerId, - String currencyCode, - OfferPayload.Direction direction, - Price price, - boolean useMarketBasedPrice, - double marketPriceMargin, - Coin amount, - Coin minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { - return coreOffersService.editOffer(offerId, - currencyCode, - direction, - price, + public void editOffer(String offerId, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long triggerPrice, + int enable, + EditType editType) { + coreOffersService.editOffer(offerId, + priceAsString, useMarketBasedPrice, marketPriceMargin, - amount, - minAmount, - buyerSecurityDeposit, - paymentAccount); + triggerPrice, + enable, + editType); } public void cancelOffer(String id) { diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index e18a760c077..365072f1967 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -20,13 +20,16 @@ import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.CreateOfferService; +import bisq.core.offer.MutableOfferPayloadFields; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; import bisq.core.offer.OfferFilter; +import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; +import bisq.core.provider.price.PriceFeedService; import bisq.core.user.User; import bisq.common.crypto.KeyRing; @@ -54,7 +57,10 @@ import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static bisq.core.offer.OfferPayload.Direction; import static bisq.core.offer.OfferPayload.Direction.BUY; +import static bisq.core.offer.OpenOffer.State.AVAILABLE; +import static bisq.core.offer.OpenOffer.State.DEACTIVATED; import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; +import static bisq.proto.grpc.EditOfferRequest.EditType; import static java.lang.String.format; import static java.util.Comparator.comparing; @@ -62,8 +68,11 @@ @Slf4j class CoreOffersService { - private final Supplier> priceComparator = () -> comparing(Offer::getPrice); - private final Supplier> reversePriceComparator = () -> comparing(Offer::getPrice).reversed(); + private final Supplier> priceComparator = () -> + comparing(Offer::getPrice); + + private final Supplier> openOfferPriceComparator = () -> + comparing(openOffer -> openOffer.getOffer().getPrice()); private final CoreContext coreContext; private final KeyRing keyRing; @@ -76,6 +85,7 @@ class CoreOffersService { private final OfferFilter offerFilter; private final OpenOfferManager openOfferManager; private final OfferUtil offerUtil; + private final PriceFeedService priceFeedService; private final User user; @Inject @@ -87,6 +97,7 @@ public CoreOffersService(CoreContext coreContext, OfferFilter offerFilter, OpenOfferManager openOfferManager, OfferUtil offerUtil, + PriceFeedService priceFeedService, User user) { this.coreContext = coreContext; this.keyRing = keyRing; @@ -96,6 +107,7 @@ public CoreOffersService(CoreContext coreContext, this.offerFilter = offerFilter; this.openOfferManager = openOfferManager; this.offerUtil = offerUtil; + this.priceFeedService = priceFeedService; this.user = user; } @@ -108,10 +120,10 @@ Offer getOffer(String id) { new IllegalStateException(format("offer with id '%s' not found", id))); } - Offer getMyOffer(String id) { - return offerBookService.getOffers().stream() + OpenOffer getMyOffer(String id) { + return openOfferManager.getObservableList().stream() .filter(o -> o.getId().equals(id)) - .filter(o -> o.isMyOffer(keyRing)) + .filter(o -> o.getOffer().isMyOffer(keyRing)) .findAny().orElseThrow(() -> new IllegalStateException(format("offer with id '%s' not found", id))); } @@ -125,11 +137,11 @@ List getOffers(String direction, String currencyCode) { .collect(Collectors.toList()); } - List getMyOffers(String direction, String currencyCode) { - return offerBookService.getOffers().stream() - .filter(o -> o.isMyOffer(keyRing)) - .filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) - .sorted(priceComparator(direction)) + List getMyOffers(String direction, String currencyCode) { + return openOfferManager.getObservableList().stream() + .filter(o -> o.getOffer().isMyOffer(keyRing)) + .filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode)) + .sorted(openOfferPriceComparator(direction)) .collect(Collectors.toList()); } @@ -137,7 +149,7 @@ OpenOffer getMyOpenOffer(String id) { return openOfferManager.getOpenOfferById(id) .filter(open -> open.getOffer().isMyOffer(keyRing)) .orElseThrow(() -> - new IllegalStateException(format("openoffer with id '%s' not found", id))); + new IllegalStateException(format("offer with id '%s' not found", id))); } // Create and place new offer. @@ -193,47 +205,56 @@ void createAndPlaceOffer(String currencyCode, } // Edit a placed offer. - Offer editOffer(String offerId, - String currencyCode, - Direction direction, - Price price, - boolean useMarketBasedPrice, - double marketPriceMargin, - Coin amount, - Coin minAmount, - double buyerSecurityDeposit, - PaymentAccount paymentAccount) { - Coin useDefaultTxFee = Coin.ZERO; - return createOfferService.createAndGetOffer(offerId, - direction, - currencyCode.toUpperCase(), - amount, - minAmount, - price, - useDefaultTxFee, - useMarketBasedPrice, - exactMultiply(marketPriceMargin, 0.01), - buyerSecurityDeposit, - paymentAccount); - } - - void cancelOffer(String id) { - Offer offer = getMyOffer(id); - openOfferManager.removeOffer(offer, + void editOffer(String offerId, + String editedPriceAsString, + boolean editedUseMarketBasedPrice, + double editedMarketPriceMargin, + long editedTriggerPrice, + int editedEnable, + EditType editType) { + OpenOffer openOffer = getMyOpenOffer(offerId); + new EditOfferValidator(openOffer, + editedPriceAsString, + editedUseMarketBasedPrice, + editedMarketPriceMargin, + editedTriggerPrice, + editType).validate(); + OfferPayload editedPayload = getMergedOfferPayload(openOffer, editedPriceAsString, + editedUseMarketBasedPrice, + editedMarketPriceMargin); + Offer editedOffer = new Offer(editedPayload); + priceFeedService.setCurrencyCode(openOffer.getOffer().getOfferPayload().getCurrencyCode()); + editedOffer.setPriceFeedService(priceFeedService); + editedOffer.setState(Offer.State.AVAILABLE); + openOfferManager.editOpenOfferStart(openOffer, () -> { + log.info("EditOpenOfferStart: offer {}", openOffer.getId()); }, errorMessage -> { - throw new IllegalStateException(errorMessage); + log.error(errorMessage); }); + // Client sent (sint32) newEnable, not a bool (with default=false). + // If newEnable = -1, do not change activation state + // If newEnable = 0, set state = AVAILABLE + // If newEnable = 1, set state = DEACTIVATED + OpenOffer.State newOfferState = editedEnable < 0 + ? openOffer.getState() + : editedEnable > 0 ? AVAILABLE : DEACTIVATED; + openOfferManager.editOpenOfferPublish(editedOffer, + editedTriggerPrice, + newOfferState, + () -> { + log.info("EditOpenOfferPublish: offer {}", openOffer.getId()); + }, + log::error); } - private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) { - if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { - String error = format("cannot create %s offer with payment account %s", - offer.getOfferPayload().getCounterCurrencyCode(), - paymentAccount.getId()); - throw new IllegalStateException(error); - } + void cancelOffer(String id) { + OpenOffer openOffer = getMyOffer(id); + openOfferManager.removeOffer(openOffer.getOffer(), + () -> { + }, + log::error); } private void placeOffer(Offer offer, @@ -252,6 +273,39 @@ private void placeOffer(Offer offer, throw new IllegalStateException(offer.getErrorMessage()); } + private OfferPayload getMergedOfferPayload(OpenOffer openOffer, + String editedPriceAsString, + boolean editedUseMarketBasedPrice, + double editedMarketPriceMargin) { + // API supports editing price, marketPriceMargin, useMarketBasedPrice payload + // fields. API does not support editing payment acct or currency code fields. + Offer offer = openOffer.getOffer(); + String currencyCode = offer.getOfferPayload().getCurrencyCode(); + Price editedPrice = Price.valueOf(currencyCode, priceStringToLong(editedPriceAsString, currencyCode)); + MutableOfferPayloadFields mutableOfferPayloadFields = new MutableOfferPayloadFields( + editedPrice.getValue(), + exactMultiply(editedMarketPriceMargin, 0.01), + editedUseMarketBasedPrice, + offer.getOfferPayload().getBaseCurrencyCode(), + offer.getOfferPayload().getCounterCurrencyCode(), + offer.getPaymentMethod().getId(), + offer.getMakerPaymentAccountId(), + offer.getOfferPayload().getCountryCode(), + offer.getOfferPayload().getAcceptedCountryCodes(), + offer.getOfferPayload().getBankId(), + offer.getOfferPayload().getAcceptedBankIds()); + return offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields); + } + + private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) { + if (!isPaymentAccountValidForOffer(offer, paymentAccount)) { + String error = format("cannot create %s offer with payment account %s", + offer.getOfferPayload().getCounterCurrencyCode(), + paymentAccount.getId()); + throw new IllegalStateException(error); + } + } + private boolean offerMatchesDirectionAndCurrency(Offer offer, String direction, String currencyCode) { @@ -261,11 +315,19 @@ private boolean offerMatchesDirectionAndCurrency(Offer offer, return offerOfWantedDirection && offerInWantedCurrency; } + private Comparator openOfferPriceComparator(String direction) { + // A buyer probably wants to see sell orders in price ascending order. + // A seller probably wants to see buy orders in price descending order. + return direction.equalsIgnoreCase(BUY.name()) + ? openOfferPriceComparator.get().reversed() + : openOfferPriceComparator.get(); + } + private Comparator priceComparator(String direction) { // A buyer probably wants to see sell orders in price ascending order. // A seller probably wants to see buy orders in price descending order. return direction.equalsIgnoreCase(BUY.name()) - ? reversePriceComparator.get() + ? priceComparator.get().reversed() : priceComparator.get(); } diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java new file mode 100644 index 00000000000..f451e330811 --- /dev/null +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -0,0 +1,135 @@ +package bisq.core.api; + +import bisq.core.offer.OpenOffer; + +import bisq.proto.grpc.EditOfferRequest; + +import java.math.BigDecimal; + +import lombok.extern.slf4j.Slf4j; + +import static java.lang.String.format; + +@Slf4j +class EditOfferValidator { + + private final OpenOffer currentlyOpenOffer; + private final String editedPriceAsString; + private final boolean editedUseMarketBasedPrice; + private final double editedMarketPriceMargin; + private final long editedTriggerPrice; + private final EditOfferRequest.EditType editType; + + private final boolean isZeroEditedFixedPriceString; + private final boolean isZeroEditedMarketPriceMargin; + private final boolean isZeroEditedTriggerPrice; + + EditOfferValidator(OpenOffer currentlyOpenOffer, + String editedPriceAsString, + boolean editedUseMarketBasedPrice, + double editedMarketPriceMargin, + long editedTriggerPrice, + EditOfferRequest.EditType editType) { + this.currentlyOpenOffer = currentlyOpenOffer; + this.editedPriceAsString = editedPriceAsString; + this.editedUseMarketBasedPrice = editedUseMarketBasedPrice; + this.editedMarketPriceMargin = editedMarketPriceMargin; + this.editedTriggerPrice = editedTriggerPrice; + this.editType = editType; + + this.isZeroEditedFixedPriceString = new BigDecimal(editedPriceAsString).doubleValue() == 0; + this.isZeroEditedMarketPriceMargin = editedMarketPriceMargin == 0; + this.isZeroEditedTriggerPrice = editedTriggerPrice == 0; + } + + void validate() { + log.info("Verifying 'editoffer' params OK for editType {}", editType); + switch (editType) { + case ACTIVATION_STATE_ONLY: { + validateEditedActivationState(); + break; + } + case FIXED_PRICE_ONLY: + case FIXED_PRICE_AND_ACTIVATION_STATE: { + validateEditedFixedPrice(); + break; + } + case MKT_PRICE_MARGIN_ONLY: + case MKT_PRICE_MARGIN_AND_ACTIVATION_STATE: + case TRIGGER_PRICE_ONLY: + case TRIGGER_PRICE_AND_ACTIVATION_STATE: { + // Make sure the edited trigger price is OK, even if not being changed. + validateEditedTriggerPrice(); + // Continue, no break. + } + case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE: + case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE: { + validateEditedMarketPriceMargin(); + break; + } + default: + break; + } + } + + private void validateEditedActivationState() { + if (!isZeroEditedFixedPriceString || !isZeroEditedMarketPriceMargin || !isZeroEditedTriggerPrice) + throw new IllegalStateException( + format("programmer error: cannot change fixed price (%s), " + + " mkt price margin (%s), or trigger price (%s) " + + " in offer with id '%s' when only changing activation state", + editedPriceAsString, + editedMarketPriceMargin, + editedTriggerPrice, + currentlyOpenOffer.getId())); + } + + private void validateEditedFixedPrice() { + if (currentlyOpenOffer.getOffer().isUseMarketBasedPrice()) + log.info("Attempting to change mkt price margin based offer with id '%s' to fixed price offer.", + currentlyOpenOffer.getId()); + + if (editedUseMarketBasedPrice) + throw new IllegalStateException( + format("programmer error: cannot change fixed price (%s)" + + " in mkt price based offer with id '%s'", + editedMarketPriceMargin, + currentlyOpenOffer.getId())); + + if (!isZeroEditedTriggerPrice) + throw new IllegalStateException( + format("programmer error: cannot change trigger price (%s)" + + " in offer with id '%s' when changing fixed price", + editedTriggerPrice, + currentlyOpenOffer.getId())); + + } + + private void validateEditedMarketPriceMargin() { + if (!currentlyOpenOffer.getOffer().isUseMarketBasedPrice()) + log.info("Attempting to change fixed price offer with id '%s' to mkt price margin based offer.", + currentlyOpenOffer.getId()); + + if (!editedUseMarketBasedPrice && !isZeroEditedTriggerPrice) + throw new IllegalStateException( + format("programmer error: cannot set a trigger price (%s)" + + " in fixed price offer with id '%s'", + editedTriggerPrice, + currentlyOpenOffer.getId())); + + if (!isZeroEditedFixedPriceString) + throw new IllegalStateException( + format("programmer error: cannot set fixed price (%s)" + + " in mkt price margin based offer with id '%s'", + editedPriceAsString, + currentlyOpenOffer.getId())); + } + + private void validateEditedTriggerPrice() { + if (editedTriggerPrice < 0) + throw new IllegalStateException( + format("programmer error: cannot set trigger price to a negative value" + + " in offer with id '%s'", + currentlyOpenOffer.getId())); + } +} diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index f8501f7df1f..e0588817041 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -18,6 +18,7 @@ package bisq.core.api.model; import bisq.core.offer.Offer; +import bisq.core.offer.OpenOffer; import bisq.common.Payload; @@ -61,7 +62,7 @@ public class OfferInfo implements Payload { private final String counterCurrencyCode; private final long date; private final String state; - + private final boolean isActivated; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.id; @@ -87,17 +88,18 @@ public OfferInfo(OfferInfoBuilder builder) { this.counterCurrencyCode = builder.counterCurrencyCode; this.date = builder.date; this.state = builder.state; - + this.isActivated = builder.isActivated; } public static OfferInfo toOfferInfo(Offer offer) { return getOfferInfoBuilder(offer).build(); } - public static OfferInfo toOfferInfo(Offer offer, long triggerPrice) { - // The Offer does not have a triggerPrice attribute, so we get - // the base OfferInfoBuilder, then add the OpenOffer's triggerPrice. - return getOfferInfoBuilder(offer).withTriggerPrice(triggerPrice).build(); + public static OfferInfo toOfferInfo(OpenOffer openOffer) { + return getOfferInfoBuilder(openOffer.getOffer()) + .withTriggerPrice(openOffer.getTriggerPrice()) + .withIsActivated(!openOffer.isDeactivated()) + .build(); } private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { @@ -156,6 +158,7 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { .setCounterCurrencyCode(counterCurrencyCode) .setDate(date) .setState(state) + .setIsActivated(isActivated) .build(); } @@ -185,6 +188,7 @@ public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { .withCounterCurrencyCode(proto.getCounterCurrencyCode()) .withDate(proto.getDate()) .withState(proto.getState()) + .withIsActivated(proto.getIsActivated()) .build(); } @@ -218,6 +222,7 @@ public static class OfferInfoBuilder { private String counterCurrencyCode; private long date; private String state; + private boolean isActivated; public OfferInfoBuilder withId(String id) { this.id = id; @@ -334,6 +339,11 @@ public OfferInfoBuilder withState(String state) { return this; } + public OfferInfoBuilder withIsActivated(boolean isActivated) { + this.isActivated = isActivated; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index 54658e4c9a9..2917bae6289 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -26,6 +26,8 @@ import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.CreateOfferReply; import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.EditOfferReply; +import bisq.proto.grpc.EditOfferRequest; import bisq.proto.grpc.GetMyOfferReply; import bisq.proto.grpc.GetMyOfferRequest; import bisq.proto.grpc.GetMyOffersReply; @@ -89,10 +91,9 @@ public void getOffer(GetOfferRequest req, public void getMyOffer(GetMyOfferRequest req, StreamObserver responseObserver) { try { - Offer offer = coreApi.getMyOffer(req.getId()); - OpenOffer openOffer = coreApi.getMyOpenOffer(req.getId()); + OpenOffer openOffer = coreApi.getMyOffer(req.getId()); var reply = GetMyOfferReply.newBuilder() - .setOffer(toOfferInfo(offer, openOffer.getTriggerPrice()).toProtoMessage()) + .setOffer(toOfferInfo(openOffer).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -125,7 +126,8 @@ public void getMyOffers(GetMyOffersRequest req, StreamObserver responseObserver) { try { List result = coreApi.getMyOffers(req.getDirection(), req.getCurrencyCode()) - .stream().map(OfferInfo::toOfferInfo) + .stream() + .map(OfferInfo::toOfferInfo) .collect(Collectors.toList()); var reply = GetMyOffersReply.newBuilder() .addAllOffers(result.stream() @@ -170,6 +172,25 @@ public void createOffer(CreateOfferRequest req, } } + @Override + public void editOffer(EditOfferRequest req, + StreamObserver responseObserver) { + try { + coreApi.editOffer(req.getId(), + req.getPrice(), + req.getUseMarketBasedPrice(), + req.getMarketPriceMargin(), + req.getTriggerPrice(), + req.getEnable(), + req.getEditType()); + var reply = EditOfferReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + @Override public void cancelOffer(CancelOfferRequest req, StreamObserver responseObserver) { @@ -198,6 +219,7 @@ final Optional rateMeteringInterceptor() { put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); + put(getEditOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); }} ))); From 9231e48c439debc44213132e4b3a7f540503cad0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 12:35:43 -0300 Subject: [PATCH 05/40] Refactor GrpcClient: request builders moved bisq.cli.request pkg Reduces size of GrpcClient while allowing for additional bot-friendly variations of the new grpc editOffer method. --- cli/src/main/java/bisq/cli/GrpcClient.java | 377 ++++++------------ .../cli/request/OffersServiceRequest.java | 317 +++++++++++++++ .../PaymentAccountsServiceRequest.java | 85 ++++ .../cli/request/TradesServiceRequest.java | 94 +++++ .../cli/request/WalletsServiceRequest.java | 192 +++++++++ 5 files changed, 816 insertions(+), 249 deletions(-) create mode 100644 cli/src/main/java/bisq/cli/request/OffersServiceRequest.java create mode 100644 cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java create mode 100644 cli/src/main/java/bisq/cli/request/TradesServiceRequest.java create mode 100644 cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java diff --git a/cli/src/main/java/bisq/cli/GrpcClient.java b/cli/src/main/java/bisq/cli/GrpcClient.java index 92784e88298..9b2195bf20d 100644 --- a/cli/src/main/java/bisq/cli/GrpcClient.java +++ b/cli/src/main/java/bisq/cli/GrpcClient.java @@ -21,63 +21,31 @@ import bisq.proto.grpc.BalancesInfo; import bisq.proto.grpc.BsqBalanceInfo; import bisq.proto.grpc.BtcBalanceInfo; -import bisq.proto.grpc.CancelOfferRequest; -import bisq.proto.grpc.ConfirmPaymentReceivedRequest; -import bisq.proto.grpc.ConfirmPaymentStartedRequest; -import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; -import bisq.proto.grpc.CreateOfferRequest; -import bisq.proto.grpc.CreatePaymentAccountRequest; -import bisq.proto.grpc.GetAddressBalanceRequest; -import bisq.proto.grpc.GetBalancesRequest; -import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; -import bisq.proto.grpc.GetFundingAddressesRequest; import bisq.proto.grpc.GetMethodHelpRequest; -import bisq.proto.grpc.GetMyOfferRequest; -import bisq.proto.grpc.GetMyOffersRequest; -import bisq.proto.grpc.GetOfferRequest; -import bisq.proto.grpc.GetOffersRequest; -import bisq.proto.grpc.GetPaymentAccountFormRequest; -import bisq.proto.grpc.GetPaymentAccountsRequest; -import bisq.proto.grpc.GetPaymentMethodsRequest; -import bisq.proto.grpc.GetTradeRequest; -import bisq.proto.grpc.GetTransactionRequest; -import bisq.proto.grpc.GetTxFeeRateRequest; -import bisq.proto.grpc.GetUnusedBsqAddressRequest; import bisq.proto.grpc.GetVersionRequest; -import bisq.proto.grpc.KeepFundsRequest; -import bisq.proto.grpc.LockWalletRequest; -import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.RegisterDisputeAgentRequest; -import bisq.proto.grpc.RemoveWalletPasswordRequest; -import bisq.proto.grpc.SendBsqRequest; -import bisq.proto.grpc.SendBtcRequest; -import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; -import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.StopRequest; import bisq.proto.grpc.TakeOfferReply; -import bisq.proto.grpc.TakeOfferRequest; import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.TxFeeRateInfo; import bisq.proto.grpc.TxInfo; -import bisq.proto.grpc.UnlockWalletRequest; -import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; -import bisq.proto.grpc.VerifyBsqSentToAddressRequest; -import bisq.proto.grpc.WithdrawFundsRequest; import protobuf.PaymentAccount; import protobuf.PaymentMethod; -import java.util.ArrayList; import java.util.List; import lombok.extern.slf4j.Slf4j; -import static bisq.cli.CryptoCurrencyUtil.isSupportedCryptoCurrency; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toList; -import static protobuf.OfferPayload.Direction.BUY; -import static protobuf.OfferPayload.Direction.SELL; +import static bisq.proto.grpc.EditOfferRequest.EditType; + + + +import bisq.cli.request.OffersServiceRequest; +import bisq.cli.request.PaymentAccountsServiceRequest; +import bisq.cli.request.TradesServiceRequest; +import bisq.cli.request.WalletsServiceRequest; @SuppressWarnings("ResultOfMethodCallIgnored") @@ -85,9 +53,19 @@ public final class GrpcClient { private final GrpcStubs grpcStubs; - - public GrpcClient(String apiHost, int apiPort, String apiPassword) { + private final OffersServiceRequest offersServiceRequest; + private final TradesServiceRequest tradesServiceRequest; + private final WalletsServiceRequest walletsServiceRequest; + private final PaymentAccountsServiceRequest paymentAccountsServiceRequest; + + public GrpcClient(String apiHost, + int apiPort, + String apiPassword) { this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword); + this.offersServiceRequest = new OffersServiceRequest(grpcStubs); + this.tradesServiceRequest = new TradesServiceRequest(grpcStubs); + this.walletsServiceRequest = new WalletsServiceRequest(grpcStubs); + this.paymentAccountsServiceRequest = new PaymentAccountsServiceRequest(grpcStubs); } public String getVersion() { @@ -96,108 +74,67 @@ public String getVersion() { } public BalancesInfo getBalances() { - return getBalances(""); + return walletsServiceRequest.getBalances(); } public BsqBalanceInfo getBsqBalances() { - return getBalances("BSQ").getBsq(); + return walletsServiceRequest.getBsqBalances(); } public BtcBalanceInfo getBtcBalances() { - return getBalances("BTC").getBtc(); + return walletsServiceRequest.getBtcBalances(); } public BalancesInfo getBalances(String currencyCode) { - var request = GetBalancesRequest.newBuilder() - .setCurrencyCode(currencyCode) - .build(); - return grpcStubs.walletsService.getBalances(request).getBalances(); + return walletsServiceRequest.getBalances(currencyCode); } public AddressBalanceInfo getAddressBalance(String address) { - var request = GetAddressBalanceRequest.newBuilder() - .setAddress(address).build(); - return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo(); + return walletsServiceRequest.getAddressBalance(address); } public double getBtcPrice(String currencyCode) { - var request = MarketPriceRequest.newBuilder() - .setCurrencyCode(currencyCode) - .build(); - return grpcStubs.priceService.getMarketPrice(request).getPrice(); + return walletsServiceRequest.getBtcPrice(currencyCode); } public List getFundingAddresses() { - var request = GetFundingAddressesRequest.newBuilder().build(); - return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList(); + return walletsServiceRequest.getFundingAddresses(); } public String getUnusedBsqAddress() { - var request = GetUnusedBsqAddressRequest.newBuilder().build(); - return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress(); + return walletsServiceRequest.getUnusedBsqAddress(); } public String getUnusedBtcAddress() { - var request = GetFundingAddressesRequest.newBuilder().build(); - var addressBalances = grpcStubs.walletsService.getFundingAddresses(request) - .getAddressBalanceInfoList(); - //noinspection OptionalGetWithoutIsPresent - return addressBalances.stream() - .filter(AddressBalanceInfo::getIsAddressUnused) - .findFirst() - .get() - .getAddress(); + return walletsServiceRequest.getUnusedBtcAddress(); } public TxInfo sendBsq(String address, String amount, String txFeeRate) { - var request = SendBsqRequest.newBuilder() - .setAddress(address) - .setAmount(amount) - .setTxFeeRate(txFeeRate) - .build(); - return grpcStubs.walletsService.sendBsq(request).getTxInfo(); + return walletsServiceRequest.sendBsq(address, amount, txFeeRate); } public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) { - var request = SendBtcRequest.newBuilder() - .setAddress(address) - .setAmount(amount) - .setTxFeeRate(txFeeRate) - .setMemo(memo) - .build(); - return grpcStubs.walletsService.sendBtc(request).getTxInfo(); + return walletsServiceRequest.sendBtc(address, amount, txFeeRate, memo); } public boolean verifyBsqSentToAddress(String address, String amount) { - var request = VerifyBsqSentToAddressRequest.newBuilder() - .setAddress(address) - .setAmount(amount) - .build(); - return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived(); + return walletsServiceRequest.verifyBsqSentToAddress(address, amount); } public TxFeeRateInfo getTxFeeRate() { - var request = GetTxFeeRateRequest.newBuilder().build(); - return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo(); + return walletsServiceRequest.getTxFeeRate(); } public TxFeeRateInfo setTxFeeRate(long txFeeRate) { - var request = SetTxFeeRatePreferenceRequest.newBuilder() - .setTxFeeRatePreference(txFeeRate) - .build(); - return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo(); + return walletsServiceRequest.setTxFeeRate(txFeeRate); } public TxFeeRateInfo unsetTxFeeRate() { - var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build(); - return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo(); + return walletsServiceRequest.unsetTxFeeRate(); } public TxInfo getTransaction(String txId) { - var request = GetTransactionRequest.newBuilder() - .setTxId(txId) - .build(); - return grpcStubs.walletsService.getTransaction(request).getTxInfo(); + return walletsServiceRequest.getTransaction(txId); } public OfferInfo createFixedPricedOffer(String direction, @@ -208,7 +145,7 @@ public OfferInfo createFixedPricedOffer(String direction, double securityDeposit, String paymentAcctId, String makerFeeCurrencyCode) { - return createOffer(direction, + return offersServiceRequest.createOffer(direction, currencyCode, amount, minAmount, @@ -217,7 +154,8 @@ public OfferInfo createFixedPricedOffer(String direction, 0.00, securityDeposit, paymentAcctId, - makerFeeCurrencyCode); + makerFeeCurrencyCode, + 0 /* no trigger price */); } public OfferInfo createMarketBasedPricedOffer(String direction, @@ -227,8 +165,9 @@ public OfferInfo createMarketBasedPricedOffer(String direction, double marketPriceMargin, double securityDeposit, String paymentAcctId, - String makerFeeCurrencyCode) { - return createOffer(direction, + String makerFeeCurrencyCode, + long triggerPrice) { + return offersServiceRequest.createOffer(direction, currencyCode, amount, minAmount, @@ -237,7 +176,8 @@ public OfferInfo createMarketBasedPricedOffer(String direction, marketPriceMargin, securityDeposit, paymentAcctId, - makerFeeCurrencyCode); + makerFeeCurrencyCode, + triggerPrice); } public OfferInfo createOffer(String direction, @@ -249,253 +189,192 @@ public OfferInfo createOffer(String direction, double marketPriceMargin, double securityDeposit, String paymentAcctId, - String makerFeeCurrencyCode) { - var request = CreateOfferRequest.newBuilder() - .setDirection(direction) - .setCurrencyCode(currencyCode) - .setAmount(amount) - .setMinAmount(minAmount) - .setUseMarketBasedPrice(useMarketBasedPrice) - .setPrice(fixedPrice) - .setMarketPriceMargin(marketPriceMargin) - .setBuyerSecurityDeposit(securityDeposit) - .setPaymentAccountId(paymentAcctId) - .setMakerFeeCurrencyCode(makerFeeCurrencyCode) - .build(); - return grpcStubs.offersService.createOffer(request).getOffer(); + String makerFeeCurrencyCode, + long triggerPrice) { + return offersServiceRequest.createOffer(direction, + currencyCode, + amount, + minAmount, + useMarketBasedPrice, + fixedPrice, + marketPriceMargin, + securityDeposit, + paymentAcctId, + makerFeeCurrencyCode, + triggerPrice); + } + + public void editOfferActivationState(String offerId, int enable) { + offersServiceRequest.editOfferActivationState(offerId, enable); + } + + public void editOfferFixedPrice(String offerId, String priceAsString) { + offersServiceRequest.editOfferFixedPrice(offerId, priceAsString); + } + + public void editOfferPriceMargin(String offerId, double marketPriceMargin) { + offersServiceRequest.editOfferPriceMargin(offerId, marketPriceMargin); + } + + public void editOfferTriggerPrice(String offerId, long triggerPrice) { + offersServiceRequest.editOfferTriggerPrice(offerId, triggerPrice); + } + + public void editOffer(String offerId, + String priceAsString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long triggerPrice, + int enable, + EditType editType) { + // Take care when using this method directly: + // useMarketBasedPrice = true if margin based offer, false for fixed priced offer + // scaledPriceString fmt = ######.#### + offersServiceRequest.editOffer(offerId, + priceAsString, + useMarketBasedPrice, + marketPriceMargin, + triggerPrice, + enable, + editType); } public void cancelOffer(String offerId) { - var request = CancelOfferRequest.newBuilder() - .setId(offerId) - .build(); - grpcStubs.offersService.cancelOffer(request); + offersServiceRequest.cancelOffer(offerId); } public OfferInfo getOffer(String offerId) { - var request = GetOfferRequest.newBuilder() - .setId(offerId) - .build(); - return grpcStubs.offersService.getOffer(request).getOffer(); + return offersServiceRequest.getOffer(offerId); } public OfferInfo getMyOffer(String offerId) { - var request = GetMyOfferRequest.newBuilder() - .setId(offerId) - .build(); - return grpcStubs.offersService.getMyOffer(request).getOffer(); + return offersServiceRequest.getMyOffer(offerId); } public List getOffers(String direction, String currencyCode) { - if (isSupportedCryptoCurrency(currencyCode)) { - return getCryptoCurrencyOffers(direction, currencyCode); - } else { - var request = GetOffersRequest.newBuilder() - .setDirection(direction) - .setCurrencyCode(currencyCode) - .build(); - return grpcStubs.offersService.getOffers(request).getOffersList(); - } + return offersServiceRequest.getOffers(direction, currencyCode); } public List getCryptoCurrencyOffers(String direction, String currencyCode) { - return getOffers(direction, "BTC").stream() - .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) - .collect(toList()); + return offersServiceRequest.getCryptoCurrencyOffers(direction, currencyCode); } public List getOffersSortedByDate(String currencyCode) { - ArrayList offers = new ArrayList<>(); - offers.addAll(getOffers(BUY.name(), currencyCode)); - offers.addAll(getOffers(SELL.name(), currencyCode)); - return sortOffersByDate(offers); + return offersServiceRequest.getOffersSortedByDate(currencyCode); } public List getOffersSortedByDate(String direction, String currencyCode) { - var offers = getOffers(direction, currencyCode); - return offers.isEmpty() ? offers : sortOffersByDate(offers); + return offersServiceRequest.getOffersSortedByDate(direction, currencyCode); } public List getBsqOffersSortedByDate() { - ArrayList offers = new ArrayList<>(); - offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ")); - offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ")); - return sortOffersByDate(offers); + return offersServiceRequest.getBsqOffersSortedByDate(); } public List getMyOffers(String direction, String currencyCode) { - if (isSupportedCryptoCurrency(currencyCode)) { - return getMyCryptoCurrencyOffers(direction, currencyCode); - } else { - var request = GetMyOffersRequest.newBuilder() - .setDirection(direction) - .setCurrencyCode(currencyCode) - .build(); - return grpcStubs.offersService.getMyOffers(request).getOffersList(); - } + return offersServiceRequest.getMyOffers(direction, currencyCode); } public List getMyCryptoCurrencyOffers(String direction, String currencyCode) { - return getMyOffers(direction, "BTC").stream() - .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) - .collect(toList()); + return offersServiceRequest.getMyCryptoCurrencyOffers(direction, currencyCode); } public List getMyOffersSortedByDate(String direction, String currencyCode) { - var offers = getMyOffers(direction, currencyCode); - return offers.isEmpty() ? offers : sortOffersByDate(offers); + return offersServiceRequest.getMyOffersSortedByDate(direction, currencyCode); } public List getMyOffersSortedByDate(String currencyCode) { - ArrayList offers = new ArrayList<>(); - offers.addAll(getMyOffers(BUY.name(), currencyCode)); - offers.addAll(getMyOffers(SELL.name(), currencyCode)); - return sortOffersByDate(offers); + return offersServiceRequest.getMyOffersSortedByDate(currencyCode); } public List getMyBsqOffersSortedByDate() { - ArrayList offers = new ArrayList<>(); - offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ")); - offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ")); - return sortOffersByDate(offers); + return offersServiceRequest.getMyBsqOffersSortedByDate(); } public OfferInfo getMostRecentOffer(String direction, String currencyCode) { - List offers = getOffersSortedByDate(direction, currencyCode); - return offers.isEmpty() ? null : offers.get(offers.size() - 1); + return offersServiceRequest.getMostRecentOffer(direction, currencyCode); } public List sortOffersByDate(List offerInfoList) { - return offerInfoList.stream() - .sorted(comparing(OfferInfo::getDate)) - .collect(toList()); + return offersServiceRequest.sortOffersByDate(offerInfoList); } public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { - var request = TakeOfferRequest.newBuilder() - .setOfferId(offerId) - .setPaymentAccountId(paymentAccountId) - .setTakerFeeCurrencyCode(takerFeeCurrencyCode) - .build(); - return grpcStubs.tradesService.takeOffer(request); + return tradesServiceRequest.getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode); } public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { - var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode); - if (reply.hasTrade()) - return reply.getTrade(); - else - throw new IllegalStateException(reply.getFailureReason().getDescription()); + return tradesServiceRequest.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode); } public TradeInfo getTrade(String tradeId) { - var request = GetTradeRequest.newBuilder() - .setTradeId(tradeId) - .build(); - return grpcStubs.tradesService.getTrade(request).getTrade(); + return tradesServiceRequest.getTrade(tradeId); } public void confirmPaymentStarted(String tradeId) { - var request = ConfirmPaymentStartedRequest.newBuilder() - .setTradeId(tradeId) - .build(); - grpcStubs.tradesService.confirmPaymentStarted(request); + tradesServiceRequest.confirmPaymentStarted(tradeId); } public void confirmPaymentReceived(String tradeId) { - var request = ConfirmPaymentReceivedRequest.newBuilder() - .setTradeId(tradeId) - .build(); - grpcStubs.tradesService.confirmPaymentReceived(request); + tradesServiceRequest.confirmPaymentReceived(tradeId); } public void keepFunds(String tradeId) { - var request = KeepFundsRequest.newBuilder() - .setTradeId(tradeId) - .build(); - grpcStubs.tradesService.keepFunds(request); + tradesServiceRequest.keepFunds(tradeId); } public void withdrawFunds(String tradeId, String address, String memo) { - var request = WithdrawFundsRequest.newBuilder() - .setTradeId(tradeId) - .setAddress(address) - .setMemo(memo) - .build(); - grpcStubs.tradesService.withdrawFunds(request); + tradesServiceRequest.withdrawFunds(tradeId, address, memo); } public List getPaymentMethods() { - var request = GetPaymentMethodsRequest.newBuilder().build(); - return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList(); + return paymentAccountsServiceRequest.getPaymentMethods(); } public String getPaymentAcctFormAsJson(String paymentMethodId) { - var request = GetPaymentAccountFormRequest.newBuilder() - .setPaymentMethodId(paymentMethodId) - .build(); - return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson(); + return paymentAccountsServiceRequest.getPaymentAcctFormAsJson(paymentMethodId); } public PaymentAccount createPaymentAccount(String json) { - var request = CreatePaymentAccountRequest.newBuilder() - .setPaymentAccountForm(json) - .build(); - return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount(); + return paymentAccountsServiceRequest.createPaymentAccount(json); } public List getPaymentAccounts() { - var request = GetPaymentAccountsRequest.newBuilder().build(); - return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList(); + return paymentAccountsServiceRequest.getPaymentAccounts(); } public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, String currencyCode, String address, boolean tradeInstant) { - var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() - .setAccountName(accountName) - .setCurrencyCode(currencyCode) - .setAddress(address) - .setTradeInstant(tradeInstant) - .build(); - return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount(); + return paymentAccountsServiceRequest.createCryptoCurrencyPaymentAccount(accountName, + currencyCode, + address, + tradeInstant); } public List getCryptoPaymentMethods() { - var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); - return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList(); + return paymentAccountsServiceRequest.getCryptoPaymentMethods(); } public void lockWallet() { - var request = LockWalletRequest.newBuilder().build(); - grpcStubs.walletsService.lockWallet(request); + walletsServiceRequest.lockWallet(); } public void unlockWallet(String walletPassword, long timeout) { - var request = UnlockWalletRequest.newBuilder() - .setPassword(walletPassword) - .setTimeout(timeout).build(); - grpcStubs.walletsService.unlockWallet(request); + walletsServiceRequest.unlockWallet(walletPassword, timeout); } public void removeWalletPassword(String walletPassword) { - var request = RemoveWalletPasswordRequest.newBuilder() - .setPassword(walletPassword).build(); - grpcStubs.walletsService.removeWalletPassword(request); + walletsServiceRequest.removeWalletPassword(walletPassword); } public void setWalletPassword(String walletPassword) { - var request = SetWalletPasswordRequest.newBuilder() - .setPassword(walletPassword).build(); - grpcStubs.walletsService.setWalletPassword(request); + walletsServiceRequest.setWalletPassword(walletPassword); } public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { - var request = SetWalletPasswordRequest.newBuilder() - .setPassword(oldWalletPassword) - .setNewPassword(newWalletPassword).build(); - grpcStubs.walletsService.setWalletPassword(request); + walletsServiceRequest.setWalletPassword(oldWalletPassword, newWalletPassword); } public void registerDisputeAgent(String disputeAgentType, String registrationKey) { diff --git a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java new file mode 100644 index 00000000000..b340f25ea7a --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java @@ -0,0 +1,317 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.cli.request; + +import bisq.proto.grpc.CancelOfferRequest; +import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.EditOfferRequest; +import bisq.proto.grpc.GetMyOfferRequest; +import bisq.proto.grpc.GetMyOffersRequest; +import bisq.proto.grpc.GetOfferRequest; +import bisq.proto.grpc.GetOffersRequest; +import bisq.proto.grpc.OfferInfo; + +import java.math.BigDecimal; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import static bisq.proto.grpc.EditOfferRequest.EditType.ACTIVATION_STATE_ONLY; +import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_ONLY; +import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_ONLY; +import static bisq.proto.grpc.EditOfferRequest.EditType.TRIGGER_PRICE_ONLY; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + + + +import bisq.cli.GrpcStubs; + +public class OffersServiceRequest { + + private final GrpcStubs grpcStubs; + + public OffersServiceRequest(GrpcStubs grpcStubs) { + this.grpcStubs = grpcStubs; + } + + public OfferInfo createFixedPricedOffer(String direction, + String currencyCode, + long amount, + long minAmount, + String fixedPrice, + double securityDeposit, + String paymentAcctId, + String makerFeeCurrencyCode) { + return createOffer(direction, + currencyCode, + amount, + minAmount, + false, + fixedPrice, + 0.00, + securityDeposit, + paymentAcctId, + makerFeeCurrencyCode, + 0 /* no trigger price */); + } + + public OfferInfo createMarketBasedPricedOffer(String direction, + String currencyCode, + long amount, + long minAmount, + double marketPriceMargin, + double securityDeposit, + String paymentAcctId, + String makerFeeCurrencyCode, + long triggerPrice) { + return createOffer(direction, + currencyCode, + amount, + minAmount, + true, + "0", + marketPriceMargin, + securityDeposit, + paymentAcctId, + makerFeeCurrencyCode, + triggerPrice); + } + + public OfferInfo createOffer(String direction, + String currencyCode, + long amount, + long minAmount, + boolean useMarketBasedPrice, + String fixedPrice, + double marketPriceMargin, + double securityDeposit, + String paymentAcctId, + String makerFeeCurrencyCode, + long triggerPrice) { + var request = CreateOfferRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .setAmount(amount) + .setMinAmount(minAmount) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setPrice(fixedPrice) + .setMarketPriceMargin(marketPriceMargin) + .setBuyerSecurityDeposit(securityDeposit) + .setPaymentAccountId(paymentAcctId) + .setMakerFeeCurrencyCode(makerFeeCurrencyCode) + .setTriggerPrice(triggerPrice) + .build(); + return grpcStubs.offersService.createOffer(request).getOffer(); + } + + // TODO Make sure this is not duplicated anywhere on CLI side. + private final Function scaledPriceStringFormat = (price) -> { + BigDecimal factor = new BigDecimal(10).pow(4); + return new BigDecimal(price).divide(factor).toPlainString(); + }; + + public void editOfferActivationState(String offerId, int enable) { + var offer = getMyOffer(offerId); + var scaledPriceString = offer.getUseMarketBasedPrice() + ? "0.00" + : scaledPriceStringFormat.apply(offer.getPrice()); + editOffer(offerId, + scaledPriceString, + offer.getUseMarketBasedPrice(), + offer.getMarketPriceMargin(), + offer.getTriggerPrice(), + enable, + ACTIVATION_STATE_ONLY); + } + + public void editOfferFixedPrice(String offerId, String rawPriceString) { + var offer = getMyOffer(offerId); + editOffer(offerId, + rawPriceString, + false, + offer.getMarketPriceMargin(), + offer.getTriggerPrice(), + offer.getIsActivated() ? 1 : 0, + FIXED_PRICE_ONLY); + } + + public void editOfferPriceMargin(String offerId, double marketPriceMargin) { + var offer = getMyOffer(offerId); + editOffer(offerId, + "0.00", + true, + marketPriceMargin, + offer.getTriggerPrice(), + offer.getIsActivated() ? 1 : 0, + MKT_PRICE_MARGIN_ONLY); + } + + public void editOfferTriggerPrice(String offerId, long triggerPrice) { + var offer = getMyOffer(offerId); + editOffer(offerId, + "0.00", + offer.getUseMarketBasedPrice(), + offer.getMarketPriceMargin(), + triggerPrice, + offer.getIsActivated() ? 1 : 0, + TRIGGER_PRICE_ONLY); + } + + public void editOffer(String offerId, + String scaledPriceString, + boolean useMarketBasedPrice, + double marketPriceMargin, + long triggerPrice, + int enable, + EditOfferRequest.EditType editType) { + // Take care when using this method directly: + // useMarketBasedPrice = true if margin based offer, false for fixed priced offer + // scaledPriceString fmt = ######.#### + var request = EditOfferRequest.newBuilder() + .setId(offerId) + .setPrice(scaledPriceString) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setMarketPriceMargin(marketPriceMargin) + .setTriggerPrice(triggerPrice) + .setEnable(enable) + .setEditType(editType) + .build(); + grpcStubs.offersService.editOffer(request); + } + + public void cancelOffer(String offerId) { + var request = CancelOfferRequest.newBuilder() + .setId(offerId) + .build(); + grpcStubs.offersService.cancelOffer(request); + } + + public OfferInfo getOffer(String offerId) { + var request = GetOfferRequest.newBuilder() + .setId(offerId) + .build(); + return grpcStubs.offersService.getOffer(request).getOffer(); + } + + public OfferInfo getMyOffer(String offerId) { + var request = GetMyOfferRequest.newBuilder() + .setId(offerId) + .build(); + return grpcStubs.offersService.getMyOffer(request).getOffer(); + } + + public List getOffers(String direction, String currencyCode) { + if (isSupportedCryptoCurrency(currencyCode)) { + return getCryptoCurrencyOffers(direction, currencyCode); + } else { + var request = GetOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getOffers(request).getOffersList(); + } + } + + public List getCryptoCurrencyOffers(String direction, String currencyCode) { + return getOffers(direction, "BTC").stream() + .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) + .collect(toList()); + } + + public List getOffersSortedByDate(String currencyCode) { + ArrayList offers = new ArrayList<>(); + offers.addAll(getOffers(BUY.name(), currencyCode)); + offers.addAll(getOffers(SELL.name(), currencyCode)); + return sortOffersByDate(offers); + } + + public List getOffersSortedByDate(String direction, String currencyCode) { + var offers = getOffers(direction, currencyCode); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getBsqOffersSortedByDate() { + ArrayList offers = new ArrayList<>(); + offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ")); + offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ")); + return sortOffersByDate(offers); + } + + public List getMyOffers(String direction, String currencyCode) { + if (isSupportedCryptoCurrency(currencyCode)) { + return getMyCryptoCurrencyOffers(direction, currencyCode); + } else { + var request = GetMyOffersRequest.newBuilder() + .setDirection(direction) + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.offersService.getMyOffers(request).getOffersList(); + } + } + + public List getMyCryptoCurrencyOffers(String direction, String currencyCode) { + return getMyOffers(direction, "BTC").stream() + .filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode)) + .collect(toList()); + } + + public List getMyOffersSortedByDate(String direction, String currencyCode) { + var offers = getMyOffers(direction, currencyCode); + return offers.isEmpty() ? offers : sortOffersByDate(offers); + } + + public List getMyOffersSortedByDate(String currencyCode) { + ArrayList offers = new ArrayList<>(); + offers.addAll(getMyOffers(BUY.name(), currencyCode)); + offers.addAll(getMyOffers(SELL.name(), currencyCode)); + return sortOffersByDate(offers); + } + + public List getMyBsqOffersSortedByDate() { + ArrayList offers = new ArrayList<>(); + offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ")); + offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ")); + return sortOffersByDate(offers); + } + + public OfferInfo getMostRecentOffer(String direction, String currencyCode) { + List offers = getOffersSortedByDate(direction, currencyCode); + return offers.isEmpty() ? null : offers.get(offers.size() - 1); + } + + public List sortOffersByDate(List offerInfoList) { + return offerInfoList.stream() + .sorted(comparing(OfferInfo::getDate)) + .collect(toList()); + } + + private static boolean isSupportedCryptoCurrency(String currencyCode) { + return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase()); + } + + private static List getSupportedCryptoCurrencies() { + final List result = new ArrayList<>(); + result.add("BSQ"); + result.sort(String::compareTo); + return result; + } +} diff --git a/cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java b/cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java new file mode 100644 index 00000000000..467aa51462e --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/PaymentAccountsServiceRequest.java @@ -0,0 +1,85 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.cli.request; + +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; +import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; +import bisq.proto.grpc.GetPaymentAccountFormRequest; +import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.GetPaymentMethodsRequest; + +import protobuf.PaymentAccount; +import protobuf.PaymentMethod; + +import java.util.List; + + + +import bisq.cli.GrpcStubs; + +public class PaymentAccountsServiceRequest { + + private final GrpcStubs grpcStubs; + + public PaymentAccountsServiceRequest(GrpcStubs grpcStubs) { + this.grpcStubs = grpcStubs; + } + + public List getPaymentMethods() { + var request = GetPaymentMethodsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList(); + } + + public String getPaymentAcctFormAsJson(String paymentMethodId) { + var request = GetPaymentAccountFormRequest.newBuilder() + .setPaymentMethodId(paymentMethodId) + .build(); + return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson(); + } + + public PaymentAccount createPaymentAccount(String json) { + var request = CreatePaymentAccountRequest.newBuilder() + .setPaymentAccountForm(json) + .build(); + return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount(); + } + + public List getPaymentAccounts() { + var request = GetPaymentAccountsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList(); + } + + public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, + String currencyCode, + String address, + boolean tradeInstant) { + var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() + .setAccountName(accountName) + .setCurrencyCode(currencyCode) + .setAddress(address) + .setTradeInstant(tradeInstant) + .build(); + return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount(); + } + + public List getCryptoPaymentMethods() { + var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); + return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList(); + } +} diff --git a/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java new file mode 100644 index 00000000000..6d57bb03547 --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/TradesServiceRequest.java @@ -0,0 +1,94 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.cli.request; + +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; +import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.KeepFundsRequest; +import bisq.proto.grpc.TakeOfferReply; +import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.TradeInfo; +import bisq.proto.grpc.WithdrawFundsRequest; + + + +import bisq.cli.GrpcStubs; + +public class TradesServiceRequest { + + private final GrpcStubs grpcStubs; + + public TradesServiceRequest(GrpcStubs grpcStubs) { + this.grpcStubs = grpcStubs; + } + + public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { + var request = TakeOfferRequest.newBuilder() + .setOfferId(offerId) + .setPaymentAccountId(paymentAccountId) + .setTakerFeeCurrencyCode(takerFeeCurrencyCode) + .build(); + return grpcStubs.tradesService.takeOffer(request); + } + + public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { + var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode); + if (reply.hasTrade()) + return reply.getTrade(); + else + throw new IllegalStateException(reply.getFailureReason().getDescription()); + } + + public TradeInfo getTrade(String tradeId) { + var request = GetTradeRequest.newBuilder() + .setTradeId(tradeId) + .build(); + return grpcStubs.tradesService.getTrade(request).getTrade(); + } + + public void confirmPaymentStarted(String tradeId) { + var request = ConfirmPaymentStartedRequest.newBuilder() + .setTradeId(tradeId) + .build(); + grpcStubs.tradesService.confirmPaymentStarted(request); + } + + public void confirmPaymentReceived(String tradeId) { + var request = ConfirmPaymentReceivedRequest.newBuilder() + .setTradeId(tradeId) + .build(); + grpcStubs.tradesService.confirmPaymentReceived(request); + } + + public void keepFunds(String tradeId) { + var request = KeepFundsRequest.newBuilder() + .setTradeId(tradeId) + .build(); + grpcStubs.tradesService.keepFunds(request); + } + + public void withdrawFunds(String tradeId, String address, String memo) { + var request = WithdrawFundsRequest.newBuilder() + .setTradeId(tradeId) + .setAddress(address) + .setMemo(memo) + .build(); + grpcStubs.tradesService.withdrawFunds(request); + } +} diff --git a/cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java b/cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java new file mode 100644 index 00000000000..e4e7f07c5f5 --- /dev/null +++ b/cli/src/main/java/bisq/cli/request/WalletsServiceRequest.java @@ -0,0 +1,192 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.cli.request; + +import bisq.proto.grpc.AddressBalanceInfo; +import bisq.proto.grpc.BalancesInfo; +import bisq.proto.grpc.BsqBalanceInfo; +import bisq.proto.grpc.BtcBalanceInfo; +import bisq.proto.grpc.GetAddressBalanceRequest; +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.GetTransactionRequest; +import bisq.proto.grpc.GetTxFeeRateRequest; +import bisq.proto.grpc.GetUnusedBsqAddressRequest; +import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.MarketPriceRequest; +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.SendBtcRequest; +import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.TxFeeRateInfo; +import bisq.proto.grpc.TxInfo; +import bisq.proto.grpc.UnlockWalletRequest; +import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.VerifyBsqSentToAddressRequest; + +import java.util.List; + + + +import bisq.cli.GrpcStubs; + +public class WalletsServiceRequest { + + private final GrpcStubs grpcStubs; + + public WalletsServiceRequest(GrpcStubs grpcStubs) { + this.grpcStubs = grpcStubs; + } + + public BalancesInfo getBalances() { + return getBalances(""); + } + + public BsqBalanceInfo getBsqBalances() { + return getBalances("BSQ").getBsq(); + } + + public BtcBalanceInfo getBtcBalances() { + return getBalances("BTC").getBtc(); + } + + public BalancesInfo getBalances(String currencyCode) { + var request = GetBalancesRequest.newBuilder() + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.walletsService.getBalances(request).getBalances(); + } + + public AddressBalanceInfo getAddressBalance(String address) { + var request = GetAddressBalanceRequest.newBuilder() + .setAddress(address).build(); + return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo(); + } + + public double getBtcPrice(String currencyCode) { + var request = MarketPriceRequest.newBuilder() + .setCurrencyCode(currencyCode) + .build(); + return grpcStubs.priceService.getMarketPrice(request).getPrice(); + } + + public List getFundingAddresses() { + var request = GetFundingAddressesRequest.newBuilder().build(); + return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList(); + } + + public String getUnusedBsqAddress() { + var request = GetUnusedBsqAddressRequest.newBuilder().build(); + return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress(); + } + + public String getUnusedBtcAddress() { + var request = GetFundingAddressesRequest.newBuilder().build(); + var addressBalances = grpcStubs.walletsService.getFundingAddresses(request) + .getAddressBalanceInfoList(); + //noinspection OptionalGetWithoutIsPresent + return addressBalances.stream() + .filter(AddressBalanceInfo::getIsAddressUnused) + .findFirst() + .get() + .getAddress(); + } + + public TxInfo sendBsq(String address, String amount, String txFeeRate) { + var request = SendBsqRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .build(); + return grpcStubs.walletsService.sendBsq(request).getTxInfo(); + } + + public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) { + var request = SendBtcRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .setTxFeeRate(txFeeRate) + .setMemo(memo) + .build(); + return grpcStubs.walletsService.sendBtc(request).getTxInfo(); + } + + public boolean verifyBsqSentToAddress(String address, String amount) { + var request = VerifyBsqSentToAddressRequest.newBuilder() + .setAddress(address) + .setAmount(amount) + .build(); + return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived(); + } + + public TxFeeRateInfo getTxFeeRate() { + var request = GetTxFeeRateRequest.newBuilder().build(); + return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo(); + } + + public TxFeeRateInfo setTxFeeRate(long txFeeRate) { + var request = SetTxFeeRatePreferenceRequest.newBuilder() + .setTxFeeRatePreference(txFeeRate) + .build(); + return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo(); + } + + public TxFeeRateInfo unsetTxFeeRate() { + var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build(); + return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo(); + } + + public TxInfo getTransaction(String txId) { + var request = GetTransactionRequest.newBuilder() + .setTxId(txId) + .build(); + return grpcStubs.walletsService.getTransaction(request).getTxInfo(); + } + + public void lockWallet() { + var request = LockWalletRequest.newBuilder().build(); + grpcStubs.walletsService.lockWallet(request); + } + + public void unlockWallet(String walletPassword, long timeout) { + var request = UnlockWalletRequest.newBuilder() + .setPassword(walletPassword) + .setTimeout(timeout).build(); + grpcStubs.walletsService.unlockWallet(request); + } + + public void removeWalletPassword(String walletPassword) { + var request = RemoveWalletPasswordRequest.newBuilder() + .setPassword(walletPassword).build(); + grpcStubs.walletsService.removeWalletPassword(request); + } + + public void setWalletPassword(String walletPassword) { + var request = SetWalletPasswordRequest.newBuilder() + .setPassword(walletPassword).build(); + grpcStubs.walletsService.setWalletPassword(request); + } + + public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { + var request = SetWalletPasswordRequest.newBuilder() + .setPassword(oldWalletPassword) + .setNewPassword(newWalletPassword).build(); + grpcStubs.walletsService.setWalletPassword(request); + } +} From d2939cc5676ce840d6af771d0321b0ffe2579b7c Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 12:43:02 -0300 Subject: [PATCH 06/40] Add new EditOfferOptionParser and test --- .../cli/opts/AbstractMethodOptionParser.java | 4 + .../bisq/cli/opts/EditOfferOptionParser.java | 264 ++++++++++++++ cli/src/main/java/bisq/cli/opts/OptLabel.java | 2 + .../cli/opt/EditOfferOptionParserTest.java | 325 ++++++++++++++++++ .../java/bisq/cli/opt/OptionParsersTest.java | 2 +- 5 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java create mode 100644 cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java diff --git a/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java b/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java index 25256eb6a99..e0b08ed7713 100644 --- a/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/AbstractMethodOptionParser.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.function.Function; +import java.util.function.Predicate; import lombok.Getter; @@ -64,6 +65,9 @@ public boolean isForHelp() { return options.has(helpOpt); } + protected final Predicate> valueNotSpecified = (opt) -> + !options.hasArgument(opt) || options.valueOf(opt).isEmpty(); + private final Function cliExceptionMessageStyle = (ex) -> { if (ex.getMessage() == null) return null; diff --git a/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java new file mode 100644 index 00000000000..8a59c891dae --- /dev/null +++ b/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java @@ -0,0 +1,264 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.cli.opts; + + +import bisq.proto.grpc.EditOfferRequest; + +import joptsimple.OptionSpec; + +import java.math.BigDecimal; + +import static bisq.cli.opts.OptLabel.*; +import static bisq.proto.grpc.EditOfferRequest.EditType.*; +import static java.lang.String.format; + + + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class EditOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { + + final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel") + .withRequiredArg(); + + final OptionSpec fixedPriceOpt = parser.accepts(OPT_FIXED_PRICE, "fixed btc price") + .withOptionalArg() + .defaultsTo("0"); + + final OptionSpec mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN, + "market btc price margin (%)") + .withOptionalArg() + .defaultsTo("0.00"); + + final OptionSpec triggerPriceOpt = parser.accepts(OPT_TRIGGER_PRICE, + "trigger price (applies to mkt price margin based offers)") + .withOptionalArg() + .defaultsTo("0"); + + // The 'enable' string opt is optional, and can be empty (meaning do not change + // activation state). For this reason, a boolean type is not used (can only be + // true or false). + final OptionSpec enableOpt = parser.accepts(OPT_ENABLE, + "enable or disable offer") + .withOptionalArg() + .ofType(String.class); + + private EditOfferRequest.EditType offerEditType; + + public EditOfferOptionParser(String[] args) { + super(args); + } + + public EditOfferOptionParser parse() { + super.parse(); + + // Short circuit opt validation if user just wants help. + if (options.has(helpOpt)) + return this; + + if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty()) + throw new IllegalArgumentException("no offer id specified"); + + boolean hasNoEditDetails = !options.has(fixedPriceOpt) + && !options.has(mktPriceMarginOpt) + && !options.has(triggerPriceOpt) + && !options.has(enableOpt); + if (hasNoEditDetails) + throw new IllegalArgumentException("no edit details specified"); + + if (options.has(enableOpt)) { + if (valueNotSpecified.test(enableOpt)) + throw new IllegalArgumentException("invalid enable value specified, must be true|false"); + + var enableOptValue = options.valueOf(enableOpt); + if (!enableOptValue.equalsIgnoreCase("true") + && !enableOptValue.equalsIgnoreCase("false")) + throw new IllegalArgumentException("invalid enable value specified, must be true|false"); + + // A single enable opt is a valid opt combo. + boolean enableOptIsOnlyOpt = !options.has(fixedPriceOpt) + && !options.has(mktPriceMarginOpt) + && !options.has(triggerPriceOpt); + if (enableOptIsOnlyOpt) { + offerEditType = ACTIVATION_STATE_ONLY; + return this; + } + } + + if (options.has(fixedPriceOpt)) { + if (valueNotSpecified.test(fixedPriceOpt)) + throw new IllegalArgumentException("no fixed price specified"); + + String fixedPriceAsString = options.valueOf(fixedPriceOpt); + try { + Double.valueOf(fixedPriceAsString); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("%s is not a number", fixedPriceAsString)); + } + + boolean fixedPriceOptIsOnlyOpt = !options.has(mktPriceMarginOpt) + && !options.has(triggerPriceOpt) + && !options.has(enableOpt); + if (fixedPriceOptIsOnlyOpt) { + offerEditType = FIXED_PRICE_ONLY; + return this; + } + + boolean fixedPriceOptAndEnableOptAreOnlyOpts = options.has(enableOpt) + && !options.has(mktPriceMarginOpt) + && !options.has(triggerPriceOpt); + if (fixedPriceOptAndEnableOptAreOnlyOpts) { + offerEditType = FIXED_PRICE_AND_ACTIVATION_STATE; + return this; + } + } + + if (options.has(mktPriceMarginOpt)) { + if (valueNotSpecified.test(mktPriceMarginOpt)) + throw new IllegalArgumentException("no mkt price margin specified"); + + String priceMarginAsString = options.valueOf(mktPriceMarginOpt); + if (priceMarginAsString.isEmpty()) + throw new IllegalArgumentException("no market price margin specified"); + + try { + Double.valueOf(priceMarginAsString); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("%s is not a number", priceMarginAsString)); + } + + boolean mktPriceMarginOptIsOnlyOpt = !options.has(triggerPriceOpt) + && !options.has(fixedPriceOpt) + && !options.has(enableOpt); + if (mktPriceMarginOptIsOnlyOpt) { + offerEditType = MKT_PRICE_MARGIN_ONLY; + return this; + } + + boolean mktPriceMarginOptAndEnableOptAreOnlyOpts = options.has(enableOpt) + && !options.has(triggerPriceOpt); + if (mktPriceMarginOptAndEnableOptAreOnlyOpts) { + offerEditType = MKT_PRICE_MARGIN_AND_ACTIVATION_STATE; + return this; + } + } + + if (options.has(triggerPriceOpt)) { + if (valueNotSpecified.test(triggerPriceOpt)) + throw new IllegalArgumentException("no trigger price specified"); + + String triggerPriceAsString = options.valueOf(fixedPriceOpt); + if (triggerPriceAsString.isEmpty()) + throw new IllegalArgumentException("trigger price not specified"); + + try { + Double.valueOf(triggerPriceAsString); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("%s is not a number", triggerPriceAsString)); + } + + boolean triggerPriceOptIsOnlyOpt = !options.has(mktPriceMarginOpt) + && !options.has(fixedPriceOpt) + && !options.has(enableOpt); + if (triggerPriceOptIsOnlyOpt) { + offerEditType = TRIGGER_PRICE_ONLY; + return this; + } + + boolean triggerPriceOptAndEnableOptAreOnlyOpts = !options.has(mktPriceMarginOpt) + && !options.has(fixedPriceOpt) + && options.has(enableOpt); + if (triggerPriceOptAndEnableOptAreOnlyOpts) { + offerEditType = TRIGGER_PRICE_AND_ACTIVATION_STATE; + return this; + } + } + + if (options.has(mktPriceMarginOpt) && options.has(fixedPriceOpt)) + throw new IllegalArgumentException("cannot specify market price margin and fixed price"); + + if (options.has(fixedPriceOpt) && options.has(triggerPriceOpt)) + throw new IllegalArgumentException("trigger price cannot be set on fixed price offers"); + + if (options.has(mktPriceMarginOpt) && options.has(triggerPriceOpt) && !options.has(enableOpt)) { + offerEditType = MKT_PRICE_MARGIN_AND_TRIGGER_PRICE; + return this; + } + + if (options.has(mktPriceMarginOpt) && options.has(triggerPriceOpt) && options.has(enableOpt)) { + offerEditType = MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE; + return this; + } + + return this; + } + + public String getOfferId() { + return options.valueOf(offerIdOpt); + } + + public String getFixedPrice() { + return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0"; + } + + public String getTriggerPrice() { + return options.has(triggerPriceOpt) ? options.valueOf(triggerPriceOpt) : "0"; + } + + public BigDecimal getTriggerPriceAsBigDecimal() { + return new BigDecimal(getTriggerPrice()); + } + + public String getMktPriceMargin() { + return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00"; + } + + public BigDecimal getMktPriceMarginAsBigDecimal() { + return isUsingMktPriceMargin() + ? new BigDecimal(options.valueOf(mktPriceMarginOpt)) + : BigDecimal.ZERO; + } + + public boolean isUsingMktPriceMargin() { + return options.has(mktPriceMarginOpt); + } + + public int getEnableAsSignedInt() { + // Client sends sint32 in grpc request, not a bool that can only be true or false. + // If enable = -1, do not change activation state + // If enable = 0, set state = AVAILABLE + // If enable = 1, set state = DEACTIVATED + @Nullable + Boolean input = isEnable(); + return input == null + ? -1 + : input ? 1 : 0; + } + + @Nullable + public Boolean isEnable() { + return options.has(enableOpt) + ? Boolean.valueOf(options.valueOf(enableOpt)) + : null; + } + + public EditOfferRequest.EditType getOfferEditType() { + return offerEditType; + } +} diff --git a/cli/src/main/java/bisq/cli/opts/OptLabel.java b/cli/src/main/java/bisq/cli/opts/OptLabel.java index 084c230aae3..70dda3e6fc3 100644 --- a/cli/src/main/java/bisq/cli/opts/OptLabel.java +++ b/cli/src/main/java/bisq/cli/opts/OptLabel.java @@ -27,6 +27,7 @@ public class OptLabel { public final static String OPT_CURRENCY_CODE = "currency-code"; public final static String OPT_DIRECTION = "direction"; public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; + public final static String OPT_ENABLE = "enable"; public final static String OPT_FEE_CURRENCY = "fee-currency"; public final static String OPT_FIXED_PRICE = "fixed-price"; public final static String OPT_HELP = "help"; @@ -47,6 +48,7 @@ public class OptLabel { public final static String OPT_TRADE_INSTANT = "trade-instant"; public final static String OPT_TIMEOUT = "timeout"; public final static String OPT_TRANSACTION_ID = "transaction-id"; + public final static String OPT_TRIGGER_PRICE = "trigger-price"; public final static String OPT_TX_FEE_RATE = "tx-fee-rate"; public final static String OPT_WALLET_PASSWORD = "wallet-password"; public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password"; diff --git a/cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java b/cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java new file mode 100644 index 00000000000..3305b0cb2cf --- /dev/null +++ b/cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java @@ -0,0 +1,325 @@ +package bisq.cli.opt; + +import org.junit.jupiter.api.Test; + +import static bisq.cli.Method.editoffer; +import static bisq.cli.opts.OptLabel.*; +import static bisq.proto.grpc.EditOfferRequest.EditType.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + + + +import bisq.cli.opts.EditOfferOptionParser; + +// This opt parser test has the most thorough coverage, +// and is a reference for other opt parser tests. +public class EditOfferOptionParserTest { + + private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz"; + + @Test + public void testEditOfferWithMissingOfferIdOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name() + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no offer id specified", exception.getMessage()); + } + + @Test + public void testEditOfferWithoutAnyOptsShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no edit details specified", exception.getMessage()); + } + + @Test + public void testEditOfferWithEmptyEnableOptValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_ENABLE + "=" // missing opt value + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("invalid enable value specified, must be true|false", + exception.getMessage()); + } + + @Test + public void testEditOfferWithMissingEnableValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_ENABLE // missing equals sign & opt value + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("invalid enable value specified, must be true|false", + exception.getMessage()); + } + + @Test + public void testEditOfferWithInvalidEnableValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_ENABLE + "=0" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("invalid enable value specified, must be true|false", + exception.getMessage()); + } + + @Test + public void testEditOfferWithMktPriceOptAndFixedPriceOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_MKT_PRICE_MARGIN + "=0.11", + "--" + OPT_FIXED_PRICE + "=50000.0000" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("cannot specify market price margin and fixed price", + exception.getMessage()); + } + + @Test + public void testEditOfferWithFixedPriceOptAndTriggerPriceOptShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_FIXED_PRICE + "=50000.0000", + "--" + OPT_TRIGGER_PRICE + "=51000.0000" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("trigger price cannot be set on fixed price offers", + exception.getMessage()); + } + + @Test + public void testEditOfferActivationStateOnly() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_ENABLE + "=" + "true" + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(ACTIVATION_STATE_ONLY, parser.getOfferEditType()); + assertEquals(1, parser.getEnableAsSignedInt()); + } + + @Test + public void testEditOfferFixedPriceWithoutOptValueShouldThrowException1() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_FIXED_PRICE + "=" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no fixed price specified", + exception.getMessage()); + } + + @Test + public void testEditOfferFixedPriceWithoutOptValueShouldThrowException2() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_FIXED_PRICE + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no fixed price specified", + exception.getMessage()); + } + + @Test + public void testEditOfferFixedPriceOnly() { + String fixedPriceAsString = "50000.0000"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_FIXED_PRICE + "=" + fixedPriceAsString + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(FIXED_PRICE_ONLY, parser.getOfferEditType()); + assertEquals(fixedPriceAsString, parser.getFixedPrice()); + } + + @Test + public void testEditOfferFixedPriceAndActivationStateOnly() { + String fixedPriceAsString = "50000.0000"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_FIXED_PRICE + "=" + fixedPriceAsString, + "--" + OPT_ENABLE + "=" + "false" + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(FIXED_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType()); + assertEquals(fixedPriceAsString, parser.getFixedPrice()); + assertEquals(0, parser.getEnableAsSignedInt()); + } + + @Test + public void testEditOfferMktPriceMarginOnly() { + String mktPriceMarginAsString = "0.25"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(MKT_PRICE_MARGIN_ONLY, parser.getOfferEditType()); + assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + } + + @Test + public void testEditOfferMktPriceMarginWithoutOptValueShouldThrowException() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_MKT_PRICE_MARGIN + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no mkt price margin specified", + exception.getMessage()); + } + + @Test + public void testEditOfferMktPriceMarginAndActivationStateOnly() { + String mktPriceMarginAsString = "0.15"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString, + "--" + OPT_ENABLE + "=" + "false" + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE, parser.getOfferEditType()); + assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + assertEquals(0, parser.getEnableAsSignedInt()); + } + + @Test + public void testEditTriggerPriceOnly() { + String triggerPriceAsString = "50000.0000"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(TRIGGER_PRICE_ONLY, parser.getOfferEditType()); + assertEquals(triggerPriceAsString, parser.getTriggerPrice()); + } + + @Test + public void testEditTriggerPriceWithoutOptValueShouldThrowException1() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_TRIGGER_PRICE + "=" + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no trigger price specified", + exception.getMessage()); + } + + @Test + public void testEditTriggerPriceWithoutOptValueShouldThrowException2() { + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_TRIGGER_PRICE + }; + Throwable exception = assertThrows(RuntimeException.class, () -> + new EditOfferOptionParser(args).parse()); + assertEquals("no trigger price specified", + exception.getMessage()); + } + + @Test + public void testEditTriggerPriceAndActivationStateOnly() { + String triggerPriceAsString = "50000.0000"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString, + "--" + OPT_ENABLE + "=" + "true" + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(TRIGGER_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType()); + assertEquals(triggerPriceAsString, parser.getTriggerPrice()); + assertEquals(1, parser.getEnableAsSignedInt()); + } + + @Test + public void testEditMKtPriceMarginAndTriggerPrice() { + String mktPriceMarginAsString = "0.25"; + String triggerPriceAsString = "50000.0000"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString, + "--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE, parser.getOfferEditType()); + assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + assertEquals(triggerPriceAsString, parser.getTriggerPrice()); + } + + @Test + public void testEditMKtPriceMarginAndTriggerPriceAndEnableState() { + String mktPriceMarginAsString = "0.25"; + String triggerPriceAsString = "50000.0000"; + String[] args = new String[]{ + PASSWORD_OPT, + editoffer.name(), + "--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID", + "--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString, + "--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString, + "--" + OPT_ENABLE + "=" + "FALSE" + }; + EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); + assertEquals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType()); + assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + assertEquals(triggerPriceAsString, parser.getTriggerPrice()); + assertFalse(parser.isEnable()); + } +} diff --git a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java index 951b56a5e3e..1df62cf2aa8 100644 --- a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java +++ b/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java @@ -178,7 +178,7 @@ public void testCreatePaymentAcctOptParserWithInvalidPaymentFormOptValueShouldTh new CreatePaymentAcctOptionParser(args).parse()); if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found", - exception.getMessage()); + exception.getMessage()); else assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found", exception.getMessage()); From 2344285ed3d7afd098a2bcc5920d8e5083720d88 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 12:55:23 -0300 Subject: [PATCH 07/40] Add editoffer method help --- .../main/resources/help/editoffer-help.txt | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 core/src/main/resources/help/editoffer-help.txt diff --git a/core/src/main/resources/help/editoffer-help.txt b/core/src/main/resources/help/editoffer-help.txt new file mode 100644 index 00000000000..b56bc6e8bb7 --- /dev/null +++ b/core/src/main/resources/help/editoffer-help.txt @@ -0,0 +1,95 @@ +editoffer + +NAME +---- +editoffer - edit an existing offer to buy or sell BTC + +SYNOPSIS +-------- +editoffer + --offer-id= + [--market-price-margin=] + [--trigger-price=] + [--fixed-price=] + [--enabled=] + +DESCRIPTION +----------- +Edit an existing offer. Offers can be changed in the following ways: + + Change a fixed-price offer to a market-price-margin based offer. + Change a market-price-margin based offer to a fixed-price offer. + Change a market-price-margin. + Change a fixed-price. + Define, change, or remove a market-price-margin based offer's trigger price. + Disable an enabled offer. + Enable a disabled offer. + +OPTIONS +------- +--offer-id + The ID of the buy or sell offer to edit. + +--market-price-margin + Changes the % above or below market BTC price, e.g., 1.00 (1%). + A --fixed-price offer can be changed to a --market-price-margin offer with this option. + The --market-price-margin and --trigger-price options can be used in the same editoffer command. + The --market-price-margin and --fixed-price options cannot be used in the same editoffer command. + +--fixed-price + Changes the fixed BTC price in fiat used to buy or sell BTC, e.g., 34000 (USD). + A --market-price-margin offer can be changed to a --fixed-price offer with this option. + The --fixed-price and --market-price-margin options cannot be used in the same editoffer command. + +--trigger-price + Sets the market price for triggering the de-activation of an offer, or defines trigger-price on an + offer that did not have a trigger-price when it was created. + A buy BTC offer is de-activated when the market price rises above the trigger-price. + A sell BTC offer is de-activated when the market price falls below the trigger-price. + Only applies to market-price-margin based offers; a fixed-price offer's trigger-price is ignored. + The --fixed-price and --trigger-price options cannot be used in the same editoffer command. + +--enabled + If true, enables a disabled offer. Does nothing if offer is already enabled. + If false, disabled an enabled offer. Does nothing if offer is already disabled. + +EXAMPLES +-------- + +To change a fixed-price offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea + to a 0.10% market-price-margin based offer: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.10 + +To change a market-price-margin based offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea + to a fixed-price offer: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --fixed-price=50000.0000 + +To set or change the trigger-price on a market-price-margin + based offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --trigger-price=50000.0000 + +To remove a trigger-price on a market-price-margin + based offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --trigger-price=0 + +To change the market-price-margin and trigger-price on an + offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.05 \ + --trigger-price=50000.0000 + +To disable an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --enable=false + +To enable a disabled offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea, + and change it from a fixed-price offer to a 0.50% market-price-margin based offer, + and set the trigger-price to 50000.0000: +$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.50 \ + --trigger-price=50000.0000 \ + --enable=true From be249c5e795065c627afe320cd9aa01ab2121894 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 12:56:29 -0300 Subject: [PATCH 08/40] Add editoffer to CLI --- cli/src/main/java/bisq/cli/CliMain.java | 42 +++++++++++-- .../main/java/bisq/cli/CurrencyFormat.java | 63 ++++++++++++------- cli/src/main/java/bisq/cli/Method.java | 1 + 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 95bfd0d7b84..2b481c45875 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -39,10 +39,7 @@ import lombok.extern.slf4j.Slf4j; -import static bisq.cli.CurrencyFormat.formatMarketPrice; -import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo; -import static bisq.cli.CurrencyFormat.toSatoshis; -import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct; +import static bisq.cli.CurrencyFormat.*; import static bisq.cli.Method.*; import static bisq.cli.TableFormat.*; import static bisq.cli.opts.OptLabel.*; @@ -59,6 +56,7 @@ import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; import bisq.cli.opts.CreateOfferOptionParser; import bisq.cli.opts.CreatePaymentAcctOptionParser; +import bisq.cli.opts.EditOfferOptionParser; import bisq.cli.opts.GetAddressBalanceOptionParser; import bisq.cli.opts.GetBTCMarketPriceOptionParser; import bisq.cli.opts.GetBalanceOptionParser; @@ -200,7 +198,7 @@ public static void run(String[] args) { } var currencyCode = opts.getCurrencyCode(); var price = client.getBtcPrice(currencyCode); - out.println(formatMarketPrice(price)); + out.println(formatInternalFiatPrice(price)); return; } case getfundingaddresses: { @@ -337,6 +335,7 @@ public static void run(String[] args) { var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal(); var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit()); var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode(); + var triggerPrice = 0; // Cannot be defined until offer is in book. var offer = client.createOffer(direction, currencyCode, amount, @@ -346,10 +345,34 @@ public static void run(String[] args) { marketPriceMargin.doubleValue(), securityDeposit, paymentAcctId, - makerFeeCurrencyCode); + makerFeeCurrencyCode, + triggerPrice); out.println(formatOfferTable(singletonList(offer), currencyCode)); return; } + case editoffer: { + var opts = new EditOfferOptionParser(args).parse(); + if (opts.isForHelp()) { + out.println(client.getMethodHelp(method)); + return; + } + var offerId = opts.getOfferId(); + var fixedPrice = opts.getFixedPrice(); + var isUsingMktPriceMargin = opts.isUsingMktPriceMargin(); + var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal(); + var triggerPrice = toInternalFiatPrice(opts.getTriggerPriceAsBigDecimal()); + var enable = opts.getEnableAsSignedInt(); + var editOfferType = opts.getOfferEditType(); + client.editOffer(offerId, + fixedPrice, + isUsingMktPriceMargin, + marketPriceMargin.doubleValue(), + triggerPrice, + enable, + editOfferType); + out.println("edited offer being re-added to offer book"); + return; + } case canceloffer: { var opts = new CancelOfferOptionParser(args).parse(); if (opts.isForHelp()) { @@ -754,6 +777,13 @@ private static void printHelp(OptionParser parser, @SuppressWarnings("SameParame stream.format(rowFormat, "", "--fixed-price= | --market-price=margin= \\", ""); stream.format(rowFormat, "", "--security-deposit= \\", ""); stream.format(rowFormat, "", "[--fee-currency=]", ""); + stream.format(rowFormat, "", "[--trigger-price=]", ""); + stream.println(); + stream.format(rowFormat, editoffer.name(), "--offer-id= \\", "Edit offer with id"); + stream.format(rowFormat, "", "[--fixed-price=] \\", ""); + stream.format(rowFormat, "", "[--market-price=margin=] \\", ""); + stream.format(rowFormat, "", "[--trigger-price=] \\", ""); + stream.format(rowFormat, "", "[--enabled=]", ""); stream.println(); stream.format(rowFormat, canceloffer.name(), "--offer-id=", "Cancel offer with id"); stream.println(); diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index 4abf20276ee..47bdc42df81 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -35,7 +35,12 @@ @VisibleForTesting public class CurrencyFormat { - private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); + // Formats numbers in US locale, human friendly style. + private static final NumberFormat FRIENDLY_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); + + // Formats numbers for internal use, i.e., grpc request parameters. + private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000"); + private static final DecimalFormat INTERNAL_ALTCOIN_DECIMAL_FORMAT = new DecimalFormat("##############0.00000000"); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); @@ -59,9 +64,9 @@ public static String formatBsq(long sats) { public static String formatBsqAmount(long bsqSats) { // BSQ sats = trade.getOffer().getVolume() - NUMBER_FORMAT.setMinimumFractionDigits(2); - NUMBER_FORMAT.setMaximumFractionDigits(2); - NUMBER_FORMAT.setRoundingMode(HALF_UP); + FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(2); + FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(2); + FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP); return SEND_BSQ_FORMAT.format((double) bsqSats / SATOSHI_DIVISOR.doubleValue()); } @@ -95,38 +100,48 @@ public static String formatCryptoCurrencyVolumeRange(long minVolume, long volume : formatCryptoCurrencyOfferVolume(volume); } - public static String formatMarketPrice(double price) { - NUMBER_FORMAT.setMinimumFractionDigits(4); - NUMBER_FORMAT.setMaximumFractionDigits(4); - return NUMBER_FORMAT.format(price); + public static String formatInternalFiatPrice(BigDecimal price) { + INTERNAL_FIAT_DECIMAL_FORMAT.setMinimumFractionDigits(4); + INTERNAL_FIAT_DECIMAL_FORMAT.setMaximumFractionDigits(4); + return INTERNAL_FIAT_DECIMAL_FORMAT.format(price); + } + + public static String formatInternalFiatPrice(double price) { + FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(4); + FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(4); + return FRIENDLY_NUMBER_FORMAT.format(price); } public static String formatPrice(long price) { - NUMBER_FORMAT.setMinimumFractionDigits(4); - NUMBER_FORMAT.setMaximumFractionDigits(4); - NUMBER_FORMAT.setRoundingMode(UNNECESSARY); - return NUMBER_FORMAT.format((double) price / 10_000); + FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(4); + FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(4); + FRIENDLY_NUMBER_FORMAT.setRoundingMode(UNNECESSARY); + return FRIENDLY_NUMBER_FORMAT.format((double) price / 10_000); } public static String formatCryptoCurrencyPrice(long price) { - NUMBER_FORMAT.setMinimumFractionDigits(8); - NUMBER_FORMAT.setMaximumFractionDigits(8); - NUMBER_FORMAT.setRoundingMode(UNNECESSARY); - return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue()); + FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(8); + FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(8); + FRIENDLY_NUMBER_FORMAT.setRoundingMode(UNNECESSARY); + return FRIENDLY_NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue()); } public static String formatOfferVolume(long volume) { - NUMBER_FORMAT.setMinimumFractionDigits(0); - NUMBER_FORMAT.setMaximumFractionDigits(0); - NUMBER_FORMAT.setRoundingMode(HALF_UP); - return NUMBER_FORMAT.format((double) volume / 10_000); + FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(0); + FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(0); + FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP); + return FRIENDLY_NUMBER_FORMAT.format((double) volume / 10_000); } public static String formatCryptoCurrencyOfferVolume(long volume) { - NUMBER_FORMAT.setMinimumFractionDigits(2); - NUMBER_FORMAT.setMaximumFractionDigits(2); - NUMBER_FORMAT.setRoundingMode(HALF_UP); - return NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue()); + FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(2); + FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(2); + FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP); + return FRIENDLY_NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue()); + } + + public static long toInternalFiatPrice(BigDecimal humanFriendlyFiatPrice) { + return humanFriendlyFiatPrice.multiply(new BigDecimal(10_000)).longValue(); } public static long toSatoshis(String btc) { diff --git a/cli/src/main/java/bisq/cli/Method.java b/cli/src/main/java/bisq/cli/Method.java index cf8b1d7df5f..76011877310 100644 --- a/cli/src/main/java/bisq/cli/Method.java +++ b/cli/src/main/java/bisq/cli/Method.java @@ -25,6 +25,7 @@ public enum Method { confirmpaymentreceived, confirmpaymentstarted, createoffer, + editoffer, createpaymentacct, createcryptopaymentacct, getaddressbalance, From 929b28cb8cb1667f0d64d3eb1bd017e56ee2d37a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 13:00:27 -0300 Subject: [PATCH 09/40] Add editoffer api tests & minor apitest refactoring --- .../method/offer/AbstractOfferTest.java | 50 +- .../apitest/method/offer/CancelOfferTest.java | 3 +- ...CreateOfferUsingMarketPriceMarginTest.java | 70 ++- .../apitest/method/offer/EditOfferTest.java | 429 ++++++++++++++++++ .../payment/CreatePaymentAccountTest.java | 23 + .../method/trade/TakeBuyBTCOfferTest.java | 3 +- .../method/trade/TakeSellBTCOfferTest.java | 3 +- .../LongRunningOfferDeactivationTest.java | 167 +++++++ .../java/bisq/apitest/scenario/OfferTest.java | 21 +- .../apitest/scenario/PaymentAccountTest.java | 1 + .../bisq/apitest/scenario/bot/BotClient.java | 6 +- .../apitest/scenario/bot/RandomOffer.java | 9 +- 12 files changed, 749 insertions(+), 36 deletions(-) create mode 100644 apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java create mode 100644 apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index f0e95dd25f8..81903f8efcc 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -17,14 +17,13 @@ package bisq.apitest.method.offer; -import bisq.core.monetary.Altcoin; - import protobuf.PaymentAccount; -import org.bitcoinj.utils.Fiat; - import java.math.BigDecimal; +import java.util.function.BiFunction; +import java.util.function.Function; + import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -37,10 +36,7 @@ import static bisq.apitest.config.BisqAppConfig.arbdaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.apitest.config.BisqAppConfig.seednode; -import static bisq.common.util.MathUtils.roundDouble; -import static bisq.common.util.MathUtils.scaleDownByPowerOf10; -import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; -import static java.math.RoundingMode.HALF_UP; +import static bisq.common.util.MathUtils.exactMultiply; @@ -49,6 +45,10 @@ @Slf4j public abstract class AbstractOfferTest extends MethodTest { + protected static final int ACTIVATE_OFFER = 1; + protected static final int DEACTIVATE_OFFER = 0; + protected static final long NO_TRIGGER_PRICE = 0; + @Setter protected static boolean isLongRunningTest; @@ -58,7 +58,7 @@ public abstract class AbstractOfferTest extends MethodTest { @BeforeAll public static void setUp() { startSupportingApps(true, - false, + true, bitcoind, seednode, arbdaemon, @@ -67,6 +67,27 @@ public static void setUp() { } + // Mkt Price Margin value of offer returned from server is scaled down by 10^-2. + protected final Function scaledDownMktPriceMargin = (mktPriceMargin) -> + exactMultiply(mktPriceMargin, 0.01); + + // Price value of offer returned from server is scaled up by 10^4. + protected final Function scaledUpFiatPrice = (price) -> { + BigDecimal factor = new BigDecimal(10).pow(4); + return price.multiply(factor).longValue(); + }; + + protected final BiFunction calcTriggerPriceAsLong = (base, delta) -> { + var triggerPriceAsDouble = new BigDecimal(base).add(new BigDecimal(delta)).doubleValue(); + return Double.valueOf(exactMultiply(triggerPriceAsDouble, 10_000)).longValue(); + }; + + protected final BiFunction calcFixedPriceAsString = (base, delta) -> { + var fixedPriceAsBigDecimal = new BigDecimal(Double.toString(base)) + .add(new BigDecimal(Double.toString(delta))); + return fixedPriceAsBigDecimal.toPlainString(); + }; + public static void createBsqPaymentAccounts() { alicesBsqAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's BSQ Account", BSQ, @@ -78,17 +99,6 @@ public static void createBsqPaymentAccounts() { false); } - protected double getScaledOfferPrice(double offerPrice, String currencyCode) { - int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; - return scaleDownByPowerOf10(offerPrice, precision); - } - - protected final double getPercentageDifference(double price1, double price2) { - return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5)) - .setScale(4, HALF_UP) - .doubleValue(); - } - @AfterAll public static void tearDown() { tearDownScaffold(); diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java index fe21e4aa8f2..8db313583cd 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CancelOfferTest.java @@ -54,7 +54,8 @@ public class CancelOfferTest extends AbstractOfferTest { 0.00, getDefaultBuyerSecurityDepositAsPercent(), paymentAccountId, - BSQ); + BSQ, + NO_TRIGGER_PRICE); }; @Test diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index 94c2519d913..df1f9079fb1 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -17,15 +17,20 @@ package bisq.apitest.method.offer; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; import bisq.core.payment.PaymentAccount; import bisq.proto.grpc.OfferInfo; +import org.bitcoinj.utils.Fiat; + import java.text.DecimalFormat; +import java.math.BigDecimal; + import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -33,11 +38,14 @@ import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.cli.TableFormat.formatOfferTable; +import static bisq.common.util.MathUtils.roundDouble; import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static java.lang.Math.abs; import static java.lang.String.format; +import static java.math.RoundingMode.HALF_UP; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -45,7 +53,7 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; -@Disabled +// @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { @@ -68,7 +76,8 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { priceMarginPctInput, getDefaultBuyerSecurityDepositAsPercent(), usdAccount.getId(), - MAKER_FEE_CURRENCY_CODE); + MAKER_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd")); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); @@ -109,7 +118,8 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { priceMarginPctInput, getDefaultBuyerSecurityDepositAsPercent(), nzdAccount.getId(), - MAKER_FEE_CURRENCY_CODE); + MAKER_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd")); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); @@ -150,7 +160,8 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { priceMarginPctInput, getDefaultBuyerSecurityDepositAsPercent(), gbpAccount.getId(), - MAKER_FEE_CURRENCY_CODE); + MAKER_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp")); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); @@ -191,7 +202,8 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { priceMarginPctInput, getDefaultBuyerSecurityDepositAsPercent(), brlAccount.getId(), - MAKER_FEE_CURRENCY_CODE); + MAKER_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl")); String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); @@ -220,6 +232,41 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); } + @Test + @Order(5) + public void testCreateUSDBTCBuyOfferWithTriggerPrice() { + PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US"); + double mktPriceAsDouble = aliceClient.getBtcPrice("usd"); + BigDecimal mktPrice = new BigDecimal(Double.toString(mktPriceAsDouble)); + BigDecimal triggerPrice = mktPrice.add(new BigDecimal("1000.9999")); + // TODO Duplicate this Price class logic in CLI. + long triggerPriceAsLong = Price.parse("USD", triggerPrice.toString()).getValue(); + + var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), + "usd", + 10_000_000L, + 5_000_000L, + 0.0, + getDefaultBuyerSecurityDepositAsPercent(), + usdAccount.getId(), + MAKER_FEE_CURRENCY_CODE, + triggerPriceAsLong); + genBtcBlocksThenWait(1, 4000); // give time to add to offer book + newOffer = aliceClient.getMyOffer(newOffer.getId()); + log.info("OFFER #5:\n{}", formatOfferTable(singletonList(newOffer), "usd")); + assertEquals(triggerPriceAsLong, newOffer.getTriggerPrice()); + } + + public static void main(String[] args) { + // TODO DELETE ME + String triggerPriceAsString = "10.1111"; + Price price = Price.parse("USD", triggerPriceAsString); + long triggerPriceAsLong = price.getValue(); + log.info("triggerPriceAsString: {}", triggerPriceAsString); + log.info("triggerPriceAsPrice: {}", price); + log.info("triggerPriceAsLong: {}", triggerPriceAsLong); + } + private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) { assertTrue(() -> { String counterCurrencyCode = offer.getCounterCurrencyCode(); @@ -239,6 +286,17 @@ private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginP }); } + private double getPercentageDifference(double price1, double price2) { + return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5)) + .setScale(4, HALF_UP) + .doubleValue(); + } + + private double getScaledOfferPrice(double offerPrice, String currencyCode) { + int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + return scaleDownByPowerOf10(offerPrice, precision); + } + private boolean isCalculatedPriceWithinErrorTolerance(double delta, double expectedDiffPct, double actualDiffPct, diff --git a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java new file mode 100644 index 00000000000..d7af5694af9 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java @@ -0,0 +1,429 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.apitest.method.offer; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.OfferInfo; + +import io.grpc.StatusRuntimeException; + +import java.math.BigDecimal; + +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_AND_ACTIVATION_STATE; +import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_AND_TRIGGER_PRICE; +import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE; +import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_ONLY; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + +@Disabled +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class EditOfferTest extends AbstractOfferTest { + + // Some test fixtures to reduce duplication. + private static final Map paymentAcctCache = new HashMap<>(); + private static final String DOLLAR = "USD"; + private static final String RUBLE = "RUB"; + private static final long AMOUNT = 10000000L; + + @Test + @Order(1) + public void testOfferDisableAndEnable() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("DE"); + OfferInfo originalOffer = createMktPricedOfferForEdit(BUY.name(), + "EUR", + paymentAcct.getId(), + 0.0, + NO_TRIGGER_PRICE); + assertFalse(originalOffer.getIsActivated()); // Not activated until prep is done. + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertTrue(originalOffer.getIsActivated()); + // Disable offer + aliceClient.editOfferActivationState(originalOffer.getId(), DEACTIVATE_OFFER); + genBtcBlocksThenWait(1, 1500); // Wait for offer book removal. + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertFalse(editedOffer.getIsActivated()); + // Re-enable offer + aliceClient.editOfferActivationState(editedOffer.getId(), ACTIVATE_OFFER); + genBtcBlocksThenWait(1, 1500); // Wait for offer book re-entry. + editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertTrue(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(2) + public void testEditTriggerPrice() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("FI"); + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + "EUR", + paymentAcct.getId(), + 0.0, + NO_TRIGGER_PRICE); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(0 /*no trigger price*/, originalOffer.getTriggerPrice()); + + // Edit the offer's trigger price, nothing else. + var mktPrice = aliceClient.getBtcPrice("EUR"); + var delta = 5_000.00; + var newTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPrice, delta); + + aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPriceAsLong); + sleep(2500); // Wait for offer book re-entry. + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); + assertTrue(editedOffer.getUseMarketBasedPrice()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(3) + public void testSetTriggerPriceToNegativeValueShouldThrowException() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("FI"); + final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + "EUR", + paymentAcct.getId(), + 0.0, + NO_TRIGGER_PRICE); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + // Edit the offer's trigger price, set to -1, check error. + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.editOfferTriggerPrice(originalOffer.getId(), -1L)); + String expectedExceptionMessage = + format("UNKNOWN: programmer error: cannot set trigger price to a negative value in offer with id '%s'", + originalOffer.getId()); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + @Test + @Order(4) + public void testEditMktPriceMargin() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + var originalMktPriceMargin = new BigDecimal("0.1").doubleValue(); + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + DOLLAR, + paymentAcct.getId(), + originalMktPriceMargin, + NO_TRIGGER_PRICE); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); + // Edit the offer's price margin, nothing else. + var newMktPriceMargin = new BigDecimal("0.5").doubleValue(); + aliceClient.editOfferPriceMargin(originalOffer.getId(), newMktPriceMargin); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(5) + public void testEditFixedPrice() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); + double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); + String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 200_000.0000); + OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), + RUBLE, + paymentAcct.getId(), + fixedPriceAsString); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + // Edit the offer's fixed price, nothing else. + String editedFixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 100_000.0000); + aliceClient.editOfferFixedPrice(originalOffer.getId(), editedFixedPriceAsString); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + var expectedNewFixedPrice = scaledUpFiatPrice.apply(new BigDecimal(editedFixedPriceAsString)); + assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(6) + public void testEditFixedPriceAndDeactivation() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); + double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); + String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 200_000.0000); + OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), + RUBLE, + paymentAcct.getId(), + fixedPriceAsString); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + // Edit the offer's fixed price and deactivate it. + String editedFixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 100_000.0000); + aliceClient.editOffer(originalOffer.getId(), + editedFixedPriceAsString, + originalOffer.getUseMarketBasedPrice(), + 0.0, + NO_TRIGGER_PRICE, + DEACTIVATE_OFFER, + FIXED_PRICE_AND_ACTIVATION_STATE); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + var expectedNewFixedPrice = scaledUpFiatPrice.apply(new BigDecimal(editedFixedPriceAsString)); + assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); + assertFalse(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(7) + public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + + var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); + var mktPriceAsDouble = aliceClient.getBtcPrice(DOLLAR); + var originalTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); + + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + DOLLAR, + paymentAcct.getId(), + originalMktPriceMargin, + originalTriggerPriceAsLong); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); + assertEquals(originalTriggerPriceAsLong, originalOffer.getTriggerPrice()); + + // Edit the offer's price margin and trigger price, and deactivate it. + var newMktPriceMargin = new BigDecimal("0.1").doubleValue(); + var newTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, -2_000.0000); + aliceClient.editOffer(originalOffer.getId(), + "0.00", + originalOffer.getUseMarketBasedPrice(), + newMktPriceMargin, + newTriggerPriceAsLong, + DEACTIVATE_OFFER, + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); + assertFalse(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(8) + public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); + final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + DOLLAR, + paymentAcct.getId(), + originalMktPriceMargin, + NO_TRIGGER_PRICE); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + // Try to edit both the fixed price and mkt price margin. + var newMktPriceMargin = new BigDecimal("0.25").doubleValue(); + var newFixedPrice = "50000.0000"; + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.editOffer(originalOffer.getId(), + newFixedPrice, + originalOffer.getUseMarketBasedPrice(), + newMktPriceMargin, + NO_TRIGGER_PRICE, + ACTIVATE_OFFER, + MKT_PRICE_MARGIN_ONLY)); + String expectedExceptionMessage = + format("UNKNOWN: programmer error: cannot set fixed price (%s) in" + + " mkt price margin based offer with id '%s'", + newFixedPrice, + originalOffer.getId()); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + @Test + @Order(9) + public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); + double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); + String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 200_000.0000); + OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), + RUBLE, + paymentAcct.getId(), + fixedPriceAsString); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + long newTriggerPrice = 1000000L; + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPrice)); + String expectedExceptionMessage = + format("UNKNOWN: programmer error: cannot set a trigger price (%s) in" + + " fixed price offer with id '%s'", + newTriggerPrice, + originalOffer.getId()); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + @Test + @Order(10) + public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("MX"); + double mktPriceAsDouble = aliceClient.getBtcPrice("MXN"); + String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 0.00); + OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), + "MXN", + paymentAcct.getId(), + fixedPriceAsString); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + // Change the offer to mkt price based and set a trigger price. + var newMktPriceMargin = new BigDecimal("0.05").doubleValue(); + var delta = 200_000.0000; // trigger price on buy offer is 200K above mkt price + var newTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, delta); + aliceClient.editOffer(originalOffer.getId(), + "0.00", + true, + newMktPriceMargin, + newTriggerPriceAsLong, + ACTIVATE_OFFER, + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertTrue(editedOffer.getUseMarketBasedPrice()); + assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); + assertTrue(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(11) + public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("GB"); + double mktPriceAsDouble = aliceClient.getBtcPrice("GBP"); + var originalMktPriceMargin = new BigDecimal("0.25").doubleValue(); + var delta = 1_000.0000; // trigger price on sell offer is 1K below mkt price + var originalTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, delta); + final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + "GBP", + paymentAcct.getId(), + originalMktPriceMargin, + originalTriggerPriceAsLong); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 0.00); + aliceClient.editOffer(originalOffer.getId(), + fixedPriceAsString, + false, + 0.00, + 0, + DEACTIVATE_OFFER, + FIXED_PRICE_AND_ACTIVATION_STATE); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertFalse(editedOffer.getUseMarketBasedPrice()); + assertEquals(0.00, editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + assertEquals(scaledUpFiatPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice()); + assertFalse(editedOffer.getIsActivated()); + } + + private OfferInfo createMktPricedOfferForEdit(String direction, + String currencyCode, + String paymentAccountId, + double marketPriceMargin, + long triggerPrice) { + return aliceClient.createMarketBasedPricedOffer(direction, + currencyCode, + AMOUNT, + AMOUNT, + marketPriceMargin, + getDefaultBuyerSecurityDepositAsPercent(), + paymentAccountId, + BSQ, + triggerPrice); + } + + private OfferInfo createFixedPricedOfferForEdit(String direction, + String currencyCode, + String paymentAccountId, + String priceAsString) { + return aliceClient.createFixedPricedOffer(direction, + currencyCode, + AMOUNT, + AMOUNT, + priceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + paymentAccountId, + BSQ); + } + + private void doSanityCheck(OfferInfo originalOffer, OfferInfo editedOffer) { + // Assert some of the immutable offer fields are unchanged. + assertEquals(originalOffer.getDirection(), editedOffer.getDirection()); + assertEquals(originalOffer.getAmount(), editedOffer.getAmount()); + assertEquals(originalOffer.getMinAmount(), editedOffer.getMinAmount()); + assertEquals(originalOffer.getTxFee(), editedOffer.getTxFee()); + assertEquals(originalOffer.getMakerFee(), editedOffer.getMakerFee()); + assertEquals(originalOffer.getPaymentAccountId(), editedOffer.getPaymentAccountId()); + assertEquals(originalOffer.getDate(), editedOffer.getDate()); + if (originalOffer.getDirection().equals(BUY.name())) + assertEquals(originalOffer.getBuyerSecurityDeposit(), editedOffer.getBuyerSecurityDeposit()); + else + assertEquals(originalOffer.getSellerSecurityDeposit(), editedOffer.getSellerSecurityDeposit()); + } + + private PaymentAccount getOrCreatePaymentAccount(String countryCode) { + if (paymentAcctCache.containsKey(countryCode)) { + return paymentAcctCache.get(countryCode); + } else { + PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, countryCode); + paymentAcctCache.put(countryCode, paymentAcct); + return paymentAcct; + } + } + + @AfterAll + public static void clearPaymentAcctCache() { + paymentAcctCache.clear(); + } +} diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index b7eb7f7ebb7..3caef6ee391 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -22,6 +22,7 @@ import bisq.core.payment.AliPayAccount; import bisq.core.payment.AustraliaPayid; import bisq.core.payment.CashDepositAccount; +import bisq.core.payment.ChaseQuickPayAccount; import bisq.core.payment.ClearXchangeAccount; import bisq.core.payment.F2FAccount; import bisq.core.payment.FasterPaymentsAccount; @@ -252,6 +253,28 @@ public void testCreateBrazilNationalBankAccount(TestInfo testInfo) { print(paymentAccount); } + @Test + public void testCreateChaseQuickPayAccount(TestInfo testInfo) { + File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID); + verifyEmptyForm(emptyForm, + CHASE_QUICK_PAY_ID, + PROPERTY_NAME_EMAIL, + PROPERTY_NAME_HOLDER_NAME); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); + COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); + String jsonString = getCompletedFormAsJsonString(); + ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(aliceClient, jsonString); + verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); + verifyAccountSingleTradeCurrency("USD", paymentAccount); + verifyCommonFormEntries(paymentAccount); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); + assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); + print(paymentAccount); + } + @Test public void testCreateClearXChangeAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java index 93d9b1b9c8b..8f03520b525 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferTest.java @@ -72,7 +72,8 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { 0.00, getDefaultBuyerSecurityDepositAsPercent(), alicesUsdAccount.getId(), - TRADE_FEE_CURRENCY_CODE); + TRADE_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java index ece3432123b..c4abd90934b 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeSellBTCOfferTest.java @@ -75,7 +75,8 @@ public void testTakeAlicesSellOffer(final TestInfo testInfo) { 0.00, getDefaultBuyerSecurityDepositAsPercent(), alicesUsdAccount.getId(), - TRADE_FEE_CURRENCY_CODE); + TRADE_FEE_CURRENCY_CODE, + NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java new file mode 100644 index 00000000000..80d70b71656 --- /dev/null +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -0,0 +1,167 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.apitest.scenario; + +import bisq.core.payment.PaymentAccount; + +import bisq.proto.grpc.OfferInfo; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.EnabledIf; + +import static bisq.apitest.config.ApiTestConfig.BTC; +import static bisq.cli.CurrencyFormat.formatPrice; +import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; +import static java.lang.System.getenv; +import static org.junit.jupiter.api.Assertions.fail; +import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.OfferPayload.Direction.SELL; + + + +import bisq.apitest.method.offer.AbstractOfferTest; + +/** + * Used to verify trigger based, automatic offer deactivation works. + * Disabled by default. + * Set ENV or IDE-ENV LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true to run. + */ +@EnabledIf("envLongRunningTestEnabled") +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class LongRunningOfferDeactivationTest extends AbstractOfferTest { + + private static final int MAX_ITERATIONS = 500; + + @Test + @Order(1) + public void testSellOfferAutoDisable(final TestInfo testInfo) { + PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); + double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); + long triggerPrice = calcTriggerPriceAsLong.apply(mktPriceAsDouble, -50.0000); + log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice)); + OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(), + "USD", + 1_000_000, + 1_000_000, + 0.00, + getDefaultBuyerSecurityDepositAsPercent(), + paymentAcct.getId(), + BTC, + triggerPrice); + log.info("SELL offer {} created with margin based price {}.", + offer.getId(), + formatPrice(offer.getPrice())); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + offer = aliceClient.getMyOffer(offer.getId()); // Offer has trigger price now. + log.info("SELL offer should be automatically disabled when mkt price falls below {}.", + formatPrice(offer.getTriggerPrice())); + + int numIterations = 0; + while (++numIterations < MAX_ITERATIONS) { + offer = aliceClient.getMyOffer(offer.getId()); + ; + var mktPrice = aliceClient.getBtcPrice("USD"); + if (offer.getIsActivated()) { + log.info("Offer still enabled at mkt price {} > {} trigger price", + mktPrice, + formatPrice(offer.getTriggerPrice())); + sleep(1000 * 60); // 60s + } else { + log.info("Successful test completion after offer disabled at mkt price {} < {} trigger price.", + mktPrice, + formatPrice(offer.getTriggerPrice())); + break; + } + if (numIterations == MAX_ITERATIONS) + fail("Offer never disabled"); + + genBtcBlocksThenWait(1, 0); + } + } + + @Test + @Order(2) + public void testBuyOfferAutoDisable(final TestInfo testInfo) { + PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); + double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); + long triggerPrice = calcTriggerPriceAsLong.apply(mktPriceAsDouble, 50.0000); + log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice)); + OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(), + "USD", + 1_000_000, + 1_000_000, + 0.00, + getDefaultBuyerSecurityDepositAsPercent(), + paymentAcct.getId(), + BTC, + triggerPrice); + log.info("BUY offer {} created with margin based price {}.", + offer.getId(), + formatPrice(offer.getPrice())); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + + offer = aliceClient.getMyOffer(offer.getId()); // Offer has trigger price now. + log.info("BUY offer should be automatically disabled when mkt price rises above {}.", + formatPrice(offer.getTriggerPrice())); + + int numIterations = 0; + while (++numIterations < MAX_ITERATIONS) { + offer = aliceClient.getMyOffer(offer.getId()); + ; + var mktPrice = aliceClient.getBtcPrice("USD"); + if (offer.getIsActivated()) { + log.info("Offer still enabled at mkt price {} < {} trigger price", + mktPrice, + formatPrice(offer.getTriggerPrice())); + sleep(1000 * 60); // 60s + } else { + log.info("Successful test completion after offer disabled at mkt price {} > {} trigger price.", + mktPrice, + formatPrice(offer.getTriggerPrice())); + break; + } + if (numIterations == MAX_ITERATIONS) + fail("Offer never disabled"); + + genBtcBlocksThenWait(1, 0); + } + } + + protected static boolean envLongRunningTestEnabled() { + String envName = "LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED"; + String envX = getenv(envName); + if (envX != null) { + log.info("Enabled, found {}.", envName); + return true; + } else { + log.info("Skipped, no environment variable {} defined.", envName); + log.info("To enable on Mac OS or Linux:" + + "\tIf running in terminal, export LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in bash shell." + + "\tIf running in Intellij, set LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in launcher's Environment variables field."); + return false; + } + } +} diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java index 15c11e65b49..292df14a511 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -32,6 +32,7 @@ import bisq.apitest.method.offer.CreateBSQOffersTest; import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest; import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; +import bisq.apitest.method.offer.EditOfferTest; import bisq.apitest.method.offer.ValidateCreateOfferTest; @Slf4j @@ -71,11 +72,12 @@ public void testCreateOfferUsingMarketPriceMargin() { test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin(); test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin(); test.testCreateBRLBTCSellOffer6Point55PctPriceMargin(); + test.testCreateUSDBTCBuyOfferWithTriggerPrice(); } @Test @Order(5) - public void testCreateBSQOffersTest() { + public void testCreateBSQOffers() { CreateBSQOffersTest test = new CreateBSQOffersTest(); CreateBSQOffersTest.createBsqPaymentAccounts(); test.testCreateBuy1BTCFor20KBSQOffer(); @@ -85,4 +87,21 @@ public void testCreateBSQOffersTest() { test.testGetAllMyBsqOffers(); test.testGetAvailableBsqOffers(); } + + @Test + @Order(6) + public void testEditOffer() { + EditOfferTest test = new EditOfferTest(); + test.testOfferDisableAndEnable(); + test.testEditTriggerPrice(); + test.testSetTriggerPriceToNegativeValueShouldThrowException(); + test.testEditMktPriceMargin(); + test.testEditFixedPrice(); + test.testEditFixedPriceAndDeactivation(); + test.testEditMktPriceMarginAndTriggerPriceAndDeactivation(); + test.testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException(); + test.testEditingTriggerPriceInFixedPriceOfferShouldThrowException(); + test.testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice(); + test.testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt(); + } } diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index 1aaf553c857..c3eb41343a9 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -51,6 +51,7 @@ public void testCreatePaymentAccount(TestInfo testInfo) { test.testCreateAustraliaPayidAccount(testInfo); test.testCreateCashDepositAccount(testInfo); test.testCreateBrazilNationalBankAccount(testInfo); + test.testCreateChaseQuickPayAccount(testInfo); test.testCreateClearXChangeAccount(testInfo); test.testCreateF2FAccount(testInfo); test.testCreateFasterPaymentsAccount(testInfo); diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java index c34dc14d28b..d6941a0a402 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java @@ -134,7 +134,8 @@ public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount, long minAmountInSatoshis, double priceMarginAsPercent, double securityDepositAsPercent, - String feeCurrency) { + String feeCurrency, + long triggerPrice) { return grpcClient.createMarketBasedPricedOffer(direction, currencyCode, amountInSatoshis, @@ -142,7 +143,8 @@ public OfferInfo createOfferAtMarketBasedPrice(PaymentAccount paymentAccount, priceMarginAsPercent, securityDepositAsPercent, paymentAccount.getId(), - feeCurrency); + feeCurrency, + triggerPrice); } /** diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java index 1942f8ad073..de728aa76e9 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/RandomOffer.java @@ -33,7 +33,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import static bisq.cli.CurrencyFormat.formatMarketPrice; +import static bisq.cli.CurrencyFormat.formatInternalFiatPrice; import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; @@ -128,7 +128,8 @@ public RandomOffer create() throws InvalidRandomOfferException { minAmount, priceMargin, getDefaultBuyerSecurityDepositAsPercent(), - feeCurrency); + feeCurrency, + 0 /*no trigger price*/); } else { this.offer = botClient.createOfferAtFixedPrice(paymentAccount, direction, @@ -167,11 +168,11 @@ private void printDescription() { log.info(description); if (useMarketBasedPrice) { log.info("Offer Price Margin = {}%", priceMargin); - log.info("Expected Offer Price = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode); + log.info("Expected Offer Price = {} {}", formatInternalFiatPrice(Double.parseDouble(fixedOfferPrice)), currencyCode); } else { log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode); } - log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode); + log.info("Current Market Price = {} {}", formatInternalFiatPrice(currentMarketPrice), currencyCode); } } From 571568a5e5201af2cef53aa1af3eb2dab82009ff Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 13:10:41 -0300 Subject: [PATCH 10/40] Remove chase quickpay acct test --- .../payment/CreatePaymentAccountTest.java | 22 ------------------- .../apitest/scenario/PaymentAccountTest.java | 1 - 2 files changed, 23 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 3caef6ee391..90440781829 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -253,28 +253,6 @@ public void testCreateBrazilNationalBankAccount(TestInfo testInfo) { print(paymentAccount); } - @Test - public void testCreateChaseQuickPayAccount(TestInfo testInfo) { - File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID); - verifyEmptyForm(emptyForm, - CHASE_QUICK_PAY_ID, - PROPERTY_NAME_EMAIL, - PROPERTY_NAME_HOLDER_NAME); - COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID); - COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct"); - COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com"); - COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe"); - COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, ""); - String jsonString = getCompletedFormAsJsonString(); - ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(aliceClient, jsonString); - verifyUserPayloadHasPaymentAccountWithId(aliceClient, paymentAccount.getId()); - verifyAccountSingleTradeCurrency("USD", paymentAccount); - verifyCommonFormEntries(paymentAccount); - assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail()); - assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName()); - print(paymentAccount); - } - @Test public void testCreateClearXChangeAccount(TestInfo testInfo) { File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID); diff --git a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java index c3eb41343a9..1aaf553c857 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/PaymentAccountTest.java @@ -51,7 +51,6 @@ public void testCreatePaymentAccount(TestInfo testInfo) { test.testCreateAustraliaPayidAccount(testInfo); test.testCreateCashDepositAccount(testInfo); test.testCreateBrazilNationalBankAccount(testInfo); - test.testCreateChaseQuickPayAccount(testInfo); test.testCreateClearXChangeAccount(testInfo); test.testCreateF2FAccount(testInfo); test.testCreateFasterPaymentsAccount(testInfo); From 9a5e2d0df107e92f04322a26a19f924d07f79a05 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 13:12:45 -0300 Subject: [PATCH 11/40] Remove unused import --- .../bisq/apitest/method/payment/CreatePaymentAccountTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java index 90440781829..b7eb7f7ebb7 100644 --- a/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java +++ b/apitest/src/test/java/bisq/apitest/method/payment/CreatePaymentAccountTest.java @@ -22,7 +22,6 @@ import bisq.core.payment.AliPayAccount; import bisq.core.payment.AustraliaPayid; import bisq.core.payment.CashDepositAccount; -import bisq.core.payment.ChaseQuickPayAccount; import bisq.core.payment.ClearXchangeAccount; import bisq.core.payment.F2FAccount; import bisq.core.payment.FasterPaymentsAccount; From 05f39854471d0bac45de65b4bf0acdd25add358e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 13:54:16 -0300 Subject: [PATCH 12/40] Fix problems found in codacy check --- .../apitest/scenario/LongRunningOfferDeactivationTest.java | 2 +- cli/src/main/java/bisq/cli/CurrencyFormat.java | 1 - core/src/main/java/bisq/core/api/CoreOffersService.java | 3 ++- core/src/main/java/bisq/core/api/EditOfferValidator.java | 7 ++----- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java index 80d70b71656..c15aaba6ade 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -82,7 +82,7 @@ public void testSellOfferAutoDisable(final TestInfo testInfo) { int numIterations = 0; while (++numIterations < MAX_ITERATIONS) { offer = aliceClient.getMyOffer(offer.getId()); - ; + var mktPrice = aliceClient.getBtcPrice("USD"); if (offer.getIsActivated()) { log.info("Offer still enabled at mkt price {} > {} trigger price", diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index 47bdc42df81..29639ec7b83 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -40,7 +40,6 @@ public class CurrencyFormat { // Formats numbers for internal use, i.e., grpc request parameters. private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000"); - private static final DecimalFormat INTERNAL_ALTCOIN_DECIMAL_FORMAT = new DecimalFormat("##############0.00000000"); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 365072f1967..39603387764 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -55,6 +55,7 @@ import static bisq.common.util.MathUtils.roundDoubleToLong; import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; +import static bisq.core.offer.Offer.State; import static bisq.core.offer.OfferPayload.Direction; import static bisq.core.offer.OfferPayload.Direction.BUY; import static bisq.core.offer.OpenOffer.State.AVAILABLE; @@ -225,7 +226,7 @@ void editOffer(String offerId, Offer editedOffer = new Offer(editedPayload); priceFeedService.setCurrencyCode(openOffer.getOffer().getOfferPayload().getCurrencyCode()); editedOffer.setPriceFeedService(priceFeedService); - editedOffer.setState(Offer.State.AVAILABLE); + editedOffer.setState(State.AVAILABLE); openOfferManager.editOpenOfferStart(openOffer, () -> { log.info("EditOpenOfferStart: offer {}", openOffer.getId()); diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index f451e330811..deea892b47b 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -57,13 +57,10 @@ void validate() { case MKT_PRICE_MARGIN_ONLY: case MKT_PRICE_MARGIN_AND_ACTIVATION_STATE: case TRIGGER_PRICE_ONLY: - case TRIGGER_PRICE_AND_ACTIVATION_STATE: { - // Make sure the edited trigger price is OK, even if not being changed. - validateEditedTriggerPrice(); - // Continue, no break. - } + case TRIGGER_PRICE_AND_ACTIVATION_STATE: case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE: case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE: { + validateEditedTriggerPrice(); validateEditedMarketPriceMargin(); break; } From 54efad097d0eafbfcc427f9879f22415dde75285 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 14:05:00 -0300 Subject: [PATCH 13/40] Fix codacy issue --- .../bisq/apitest/scenario/LongRunningOfferDeactivationTest.java | 2 +- core/src/main/java/bisq/core/api/CoreOffersService.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java index c15aaba6ade..2aeea5a436c 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -130,7 +130,7 @@ public void testBuyOfferAutoDisable(final TestInfo testInfo) { int numIterations = 0; while (++numIterations < MAX_ITERATIONS) { offer = aliceClient.getMyOffer(offer.getId()); - ; + var mktPrice = aliceClient.getBtcPrice("USD"); if (offer.getIsActivated()) { log.info("Offer still enabled at mkt price {} < {} trigger price", diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 39603387764..0464efb4812 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -56,6 +56,7 @@ import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static bisq.core.offer.Offer.State; +import static bisq.core.offer.Offer.State.*; import static bisq.core.offer.OfferPayload.Direction; import static bisq.core.offer.OfferPayload.Direction.BUY; import static bisq.core.offer.OpenOffer.State.AVAILABLE; From 21ac46ac0f7488627ac8a9855edef0b48be86051 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 13 Jun 2021 17:22:04 -0300 Subject: [PATCH 14/40] Fix log arg spec bug --- core/src/main/java/bisq/core/api/EditOfferValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index deea892b47b..0ba354d76b7 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -83,7 +83,7 @@ private void validateEditedActivationState() { private void validateEditedFixedPrice() { if (currentlyOpenOffer.getOffer().isUseMarketBasedPrice()) - log.info("Attempting to change mkt price margin based offer with id '%s' to fixed price offer.", + log.info("Attempting to change mkt price margin based offer with id '{}' to fixed price offer.", currentlyOpenOffer.getId()); if (editedUseMarketBasedPrice) @@ -104,7 +104,7 @@ private void validateEditedFixedPrice() { private void validateEditedMarketPriceMargin() { if (!currentlyOpenOffer.getOffer().isUseMarketBasedPrice()) - log.info("Attempting to change fixed price offer with id '%s' to mkt price margin based offer.", + log.info("Attempting to change fixed price offer with id '{}' to mkt price margin based offer.", currentlyOpenOffer.getId()); if (!editedUseMarketBasedPrice && !isZeroEditedTriggerPrice) From 32688a713f4c09e75414fee37c7f5a2647d5a754 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:17:02 -0300 Subject: [PATCH 15/40] Add bool isMyOffer to OfferInfo proto --- .../java/bisq/core/api/model/OfferInfo.java | 21 +++++++++++++++---- proto/src/main/proto/grpc.proto | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index e0588817041..ce645e6ab69 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -63,6 +63,7 @@ public class OfferInfo implements Payload { private final long date; private final String state; private final boolean isActivated; + private final boolean isMyOffer; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.id; @@ -89,20 +90,23 @@ public OfferInfo(OfferInfoBuilder builder) { this.date = builder.date; this.state = builder.state; this.isActivated = builder.isActivated; + this.isMyOffer = builder.isMyOffer; } public static OfferInfo toOfferInfo(Offer offer) { - return getOfferInfoBuilder(offer).build(); + // Offer is not mine. + return getOfferInfoBuilder(offer, false).build(); } public static OfferInfo toOfferInfo(OpenOffer openOffer) { - return getOfferInfoBuilder(openOffer.getOffer()) + // OpenOffer is mine. + return getOfferInfoBuilder(openOffer.getOffer(), true) .withTriggerPrice(openOffer.getTriggerPrice()) .withIsActivated(!openOffer.isDeactivated()) .build(); } - private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { + private static OfferInfoBuilder getOfferInfoBuilder(Offer offer, boolean isMyOffer) { return new OfferInfoBuilder() .withId(offer.getId()) .withDirection(offer.getDirection().name()) @@ -125,7 +129,8 @@ private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) .withDate(offer.getDate().getTime()) - .withState(offer.getState().name()); + .withState(offer.getState().name()) + .withIsMyOffer(isMyOffer); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -159,6 +164,7 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { .setDate(date) .setState(state) .setIsActivated(isActivated) + .setIsMyOffer(isMyOffer) .build(); } @@ -189,6 +195,7 @@ public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { .withDate(proto.getDate()) .withState(proto.getState()) .withIsActivated(proto.getIsActivated()) + .withIsMyOffer(proto.getIsMyOffer()) .build(); } @@ -223,6 +230,7 @@ public static class OfferInfoBuilder { private long date; private String state; private boolean isActivated; + private boolean isMyOffer; public OfferInfoBuilder withId(String id) { this.id = id; @@ -344,6 +352,11 @@ public OfferInfoBuilder withIsActivated(boolean isActivated) { return this; } + public OfferInfoBuilder withIsMyOffer(boolean isMyOffer) { + this.isMyOffer = isMyOffer; + return this; + } + public OfferInfo build() { return new OfferInfo(this); } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 0fa653e4c24..ff2467f713f 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -191,6 +191,7 @@ message OfferInfo { uint64 txFee = 22; uint64 makerFee = 23; bool isActivated = 24; + bool isMyOffer = 25; } message AvailabilityResultWithDescription { From 738d2f70ef7804a8e69707855ef944dbd4b01221 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:18:07 -0300 Subject: [PATCH 16/40] Fix editoffer validation bugs, tidy up CoreOffersService --- .../java/bisq/core/api/CoreOffersService.java | 78 ++++++++++++------- .../bisq/core/api/EditOfferValidator.java | 29 +++---- 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 0464efb4812..adcbc53f8c8 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -56,13 +56,13 @@ import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static bisq.core.offer.Offer.State; -import static bisq.core.offer.Offer.State.*; import static bisq.core.offer.OfferPayload.Direction; import static bisq.core.offer.OfferPayload.Direction.BUY; import static bisq.core.offer.OpenOffer.State.AVAILABLE; import static bisq.core.offer.OpenOffer.State.DEACTIVATED; import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; import static bisq.proto.grpc.EditOfferRequest.EditType; +import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static java.lang.String.format; import static java.util.Comparator.comparing; @@ -220,34 +220,45 @@ void editOffer(String offerId, editedUseMarketBasedPrice, editedMarketPriceMargin, editedTriggerPrice, + editedEnable, editType).validate(); - OfferPayload editedPayload = getMergedOfferPayload(openOffer, editedPriceAsString, + log.info("'editoffer' params OK for offerId={}" + + "\n\teditedPriceAsString={}" + + "\n\teditedUseMarketBasedPrice={}" + + "\n\teditedMarketPriceMargin={}" + + "\n\teditedTriggerPrice={}" + + "\n\teditedEnable={}" + + "\n\teditType={}", + offerId, + editedPriceAsString, editedUseMarketBasedPrice, - editedMarketPriceMargin); + editedMarketPriceMargin, + editedTriggerPrice, + editedEnable, + editType); + OpenOffer.State currentOfferState = openOffer.getState(); + // Client sent (sint32) editedEnable, not a bool (with default=false). + // If editedEnable = -1, do not change current state + // If editedEnable = 0, set state = AVAILABLE + // If editedEnable = 1, set state = DEACTIVATED + OpenOffer.State newOfferState = editedEnable < 0 + ? currentOfferState + : editedEnable > 0 ? AVAILABLE : DEACTIVATED; + OfferPayload editedPayload = getMergedOfferPayload(openOffer, + editedPriceAsString, + editedMarketPriceMargin, + editType); Offer editedOffer = new Offer(editedPayload); priceFeedService.setCurrencyCode(openOffer.getOffer().getOfferPayload().getCurrencyCode()); editedOffer.setPriceFeedService(priceFeedService); editedOffer.setState(State.AVAILABLE); openOfferManager.editOpenOfferStart(openOffer, - () -> { - log.info("EditOpenOfferStart: offer {}", openOffer.getId()); - }, - errorMessage -> { - log.error(errorMessage); - }); - // Client sent (sint32) newEnable, not a bool (with default=false). - // If newEnable = -1, do not change activation state - // If newEnable = 0, set state = AVAILABLE - // If newEnable = 1, set state = DEACTIVATED - OpenOffer.State newOfferState = editedEnable < 0 - ? openOffer.getState() - : editedEnable > 0 ? AVAILABLE : DEACTIVATED; + () -> log.info("EditOpenOfferStart: offer {}", openOffer.getId()), + log::error); openOfferManager.editOpenOfferPublish(editedOffer, editedTriggerPrice, newOfferState, - () -> { - log.info("EditOpenOfferPublish: offer {}", openOffer.getId()); - }, + () -> log.info("EditOpenOfferPublish: offer {}", openOffer.getId()), log::error); } @@ -277,17 +288,31 @@ private void placeOffer(Offer offer, private OfferPayload getMergedOfferPayload(OpenOffer openOffer, String editedPriceAsString, - boolean editedUseMarketBasedPrice, - double editedMarketPriceMargin) { - // API supports editing price, marketPriceMargin, useMarketBasedPrice payload - // fields. API does not support editing payment acct or currency code fields. + double editedMarketPriceMargin, + EditType editType) { + // API supports editing (1) price, OR (2) marketPriceMargin & useMarketBasedPrice + // OfferPayload fields. API does not support editing payment acct or currency + // code fields. Note: triggerPrice isDeactivated fields are in OpenOffer, not + // in OfferPayload. Offer offer = openOffer.getOffer(); String currencyCode = offer.getOfferPayload().getCurrencyCode(); - Price editedPrice = Price.valueOf(currencyCode, priceStringToLong(editedPriceAsString, currencyCode)); + boolean isEditingPrice = editType.equals(FIXED_PRICE_ONLY) || editType.equals(FIXED_PRICE_AND_ACTIVATION_STATE); + Price editedPrice; + if (isEditingPrice) { + editedPrice = Price.valueOf(currencyCode, priceStringToLong(editedPriceAsString, currencyCode)); + } else { + editedPrice = offer.getPrice(); + } + boolean isUsingMktPriceMargin = editType.equals(MKT_PRICE_MARGIN_ONLY) + || editType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE) + || editType.equals(TRIGGER_PRICE_ONLY) + || editType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) + || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); MutableOfferPayloadFields mutableOfferPayloadFields = new MutableOfferPayloadFields( editedPrice.getValue(), - exactMultiply(editedMarketPriceMargin, 0.01), - editedUseMarketBasedPrice, + isUsingMktPriceMargin ? exactMultiply(editedMarketPriceMargin, 0.01) : 0.00, + isUsingMktPriceMargin, offer.getOfferPayload().getBaseCurrencyCode(), offer.getOfferPayload().getCounterCurrencyCode(), offer.getPaymentMethod().getId(), @@ -296,6 +321,7 @@ private OfferPayload getMergedOfferPayload(OpenOffer openOffer, offer.getOfferPayload().getAcceptedCountryCodes(), offer.getOfferPayload().getBankId(), offer.getOfferPayload().getAcceptedBankIds()); + log.info("Merging OfferPayload with {}", mutableOfferPayloadFields); return offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields); } diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index 0ba354d76b7..82f6b78b737 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -18,6 +18,7 @@ class EditOfferValidator { private final boolean editedUseMarketBasedPrice; private final double editedMarketPriceMargin; private final long editedTriggerPrice; + private final int editedEnable; private final EditOfferRequest.EditType editType; private final boolean isZeroEditedFixedPriceString; @@ -29,12 +30,14 @@ class EditOfferValidator { boolean editedUseMarketBasedPrice, double editedMarketPriceMargin, long editedTriggerPrice, + int editedEnable, EditOfferRequest.EditType editType) { this.currentlyOpenOffer = currentlyOpenOffer; this.editedPriceAsString = editedPriceAsString; this.editedUseMarketBasedPrice = editedUseMarketBasedPrice; this.editedMarketPriceMargin = editedMarketPriceMargin; this.editedTriggerPrice = editedTriggerPrice; + this.editedEnable = editedEnable; this.editType = editType; this.isZeroEditedFixedPriceString = new BigDecimal(editedPriceAsString).doubleValue() == 0; @@ -70,14 +73,10 @@ void validate() { } private void validateEditedActivationState() { - if (!isZeroEditedFixedPriceString || !isZeroEditedMarketPriceMargin || !isZeroEditedTriggerPrice) + if (editedEnable < 0) throw new IllegalStateException( - format("programmer error: cannot change fixed price (%s), " - + " mkt price margin (%s), or trigger price (%s) " - + " in offer with id '%s' when only changing activation state", - editedPriceAsString, - editedMarketPriceMargin, - editedTriggerPrice, + format("programmer error: the 'enable' request parameter does not" + + " indicate activation state of offer with id '{}' should be changed.", currentlyOpenOffer.getId())); } @@ -107,13 +106,6 @@ private void validateEditedMarketPriceMargin() { log.info("Attempting to change fixed price offer with id '{}' to mkt price margin based offer.", currentlyOpenOffer.getId()); - if (!editedUseMarketBasedPrice && !isZeroEditedTriggerPrice) - throw new IllegalStateException( - format("programmer error: cannot set a trigger price (%s)" - + " in fixed price offer with id '%s'", - editedTriggerPrice, - currentlyOpenOffer.getId())); - if (!isZeroEditedFixedPriceString) throw new IllegalStateException( format("programmer error: cannot set fixed price (%s)" @@ -123,6 +115,15 @@ private void validateEditedMarketPriceMargin() { } private void validateEditedTriggerPrice() { + if (!currentlyOpenOffer.getOffer().isUseMarketBasedPrice() + && !editedUseMarketBasedPrice + && !isZeroEditedTriggerPrice) + throw new IllegalStateException( + format("programmer error: cannot set a trigger price (%s)" + + " in fixed price offer with id '%s'", + editedTriggerPrice, + currentlyOpenOffer.getId())); + if (editedTriggerPrice < 0) throw new IllegalStateException( format("programmer error: cannot set trigger price to a negative value" From e2a205a31d04d94bbcdb825be45a297c9a630047 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:24:27 -0300 Subject: [PATCH 17/40] Show enable/trigger-price cols for 'getmyoffer' --- cli/src/main/java/bisq/cli/CliMain.java | 2 +- .../java/bisq/cli/ColumnHeaderConstants.java | 3 +- cli/src/main/java/bisq/cli/TableFormat.java | 109 ++++++++++++++---- 3 files changed, 88 insertions(+), 26 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index 2b481c45875..bf3978675c6 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -370,7 +370,7 @@ public static void run(String[] args) { triggerPrice, enable, editOfferType); - out.println("edited offer being re-added to offer book"); + out.println("offer has been edited"); return; } case canceloffer: { diff --git a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java index 775221b5ed5..32a4564d16d 100644 --- a/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java +++ b/cli/src/main/java/bisq/cli/ColumnHeaderConstants.java @@ -46,6 +46,7 @@ class ColumnHeaderConstants { static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); static final String COL_HEADER_CURRENCY = "Currency"; static final String COL_HEADER_DIRECTION = "Buy/Sell"; + static final String COL_HEADER_ENABLED = "Enabled"; static final String COL_HEADER_NAME = "Name"; static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC"; @@ -64,7 +65,7 @@ class ColumnHeaderConstants { static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' '); static final String COL_HEADER_TRADE_MAKER_FEE = padEnd("Maker Fee(%-3s)", 12, ' '); // "Maker Fee(%-3s)"; static final String COL_HEADER_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)"; - + static final String COL_HEADER_TRIGGER_PRICE = "Trigger Price(%-3s)"; static final String COL_HEADER_TX_ID = "Tx ID"; static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)"; static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)"; diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 5c123184e94..d1d4aa729ac 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -147,48 +147,109 @@ public static String formatPaymentAcctTbl(List paymentAccounts) public static String formatOfferTable(List offers, String currencyCode) { if (offers == null || offers.isEmpty()) - throw new IllegalArgumentException(format("%s offers argument is empty", currencyCode.toLowerCase())); + throw new IllegalArgumentException(format("%s offer list is empty", currencyCode.toLowerCase())); String baseCurrencyCode = offers.get(0).getBaseCurrencyCode(); + boolean isMyOffer = offers.get(0).getIsMyOffer(); return baseCurrencyCode.equalsIgnoreCase("BTC") - ? formatFiatOfferTable(offers, currencyCode) + ? formatFiatOfferTable(offers, currencyCode, isMyOffer) : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode); } - private static String formatFiatOfferTable(List offers, String fiatCurrencyCode) { + private static String formatFiatOfferTable(List offers, + String fiatCurrencyCode, + boolean isMyOffer) { // Some column values might be longer than header, so we need to calculate them. int amountColWith = getLongestAmountColWidth(offers); int volumeColWidth = getLongestVolumeColWidth(offers); int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); - String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER - + COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrencyCode + // "Enabled" and "Trigger Price" columns are displayed for my offers only. + String enabledHeaderFormat = isMyOffer ? + COL_HEADER_ENABLED + COL_HEADER_DELIMITER + : ""; + String triggerPriceHeaderFormat = isMyOffer ? + // COL_HEADER_TRIGGER_PRICE includes %s -> fiatCurrencyCode + COL_HEADER_TRIGGER_PRICE + COL_HEADER_DELIMITER + : ""; + String headersFormat = enabledHeaderFormat + + COL_HEADER_DIRECTION + COL_HEADER_DELIMITER + // COL_HEADER_PRICE includes %s -> fiatCurrencyCode + + COL_HEADER_PRICE + COL_HEADER_DELIMITER + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER // COL_HEADER_VOLUME includes %s -> fiatCurrencyCode + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER + + triggerPriceHeaderFormat + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + COL_HEADER_UUID.trim() + "%n"; String headerLine = format(headersFormat, fiatCurrencyCode.toUpperCase(), - fiatCurrencyCode.toUpperCase()); - String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" - + "%" + (COL_HEADER_PRICE.length() - 1) + "s" - + " %" + amountColWith + "s" - + " %" + (volumeColWidth - 1) + "s" - + " %-" + paymentMethodColWidth + "s" - + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" - + " %-" + COL_HEADER_UUID.length() + "s"; - return headerLine - + offers.stream() - .map(o -> format(colDataFormat, - o.getDirection(), - formatPrice(o.getPrice()), - formatAmountRange(o.getMinAmount(), o.getAmount()), - formatVolumeRange(o.getMinVolume(), o.getVolume()), - o.getPaymentMethodShortName(), - formatTimestamp(o.getDate()), - o.getId())) - .collect(Collectors.joining("\n")); + fiatCurrencyCode.toUpperCase(), + // COL_HEADER_TRIGGER_PRICE includes %s -> fiatCurrencyCode + isMyOffer ? fiatCurrencyCode.toUpperCase() : ""); + String colDataFormat = getFiatOfferColDataFormat(isMyOffer, + amountColWith, + volumeColWidth, + paymentMethodColWidth); + return formattedFiatOfferTable(offers, isMyOffer, headerLine, colDataFormat); + } + + private static String formattedFiatOfferTable(List offers, + boolean isMyOffer, + String headerLine, + String colDataFormat) { + if (isMyOffer) { + return headerLine + + offers.stream() + .map(o -> format(colDataFormat, + o.getIsActivated() ? "YES" : "NO", + o.getDirection(), + formatPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatVolumeRange(o.getMinVolume(), o.getVolume()), + o.getTriggerPrice() == 0 ? "" : formatPrice(o.getTriggerPrice()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) + .collect(Collectors.joining("\n")); + } else { + return headerLine + + offers.stream() + .map(o -> format(colDataFormat, + o.getDirection(), + formatPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) + .collect(Collectors.joining("\n")); + } + } + + private static String getFiatOfferColDataFormat(boolean isMyOffer, + int amountColWith, + int volumeColWidth, + int paymentMethodColWidth) { + if (isMyOffer) { + return "%-" + (COL_HEADER_ENABLED.length() + COL_HEADER_DELIMITER.length()) + "s" + + "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %" + (COL_HEADER_TRIGGER_PRICE.length() - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; + } else { + return "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" + + "%" + (COL_HEADER_PRICE.length() - 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; + } } private static String formatCryptoCurrencyOfferTable(List offers, String cryptoCurrencyCode) { From 4da64b9bd0fd277cce9e0bc66204625fc52589aa Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:27:23 -0300 Subject: [PATCH 18/40] Improve 'editoffer' opt parsing, fix test pkg name --- .../bisq/cli/opts/EditOfferOptionParser.java | 65 ++++++++++++------- .../EditOfferOptionParserTest.java | 47 ++++++++++---- .../cli/{opt => opts}/OptionParsersTest.java | 9 +-- 3 files changed, 76 insertions(+), 45 deletions(-) rename cli/src/test/java/bisq/cli/{opt => opts}/EditOfferOptionParserTest.java (87%) rename cli/src/test/java/bisq/cli/{opt => opts}/OptionParsersTest.java (97%) diff --git a/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java b/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java index 8a59c891dae..288f1a9f40d 100644 --- a/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java +++ b/cli/src/main/java/bisq/cli/opts/EditOfferOptionParser.java @@ -34,6 +34,10 @@ public class EditOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts { + static int OPT_ENABLE_ON = 1; + static int OPT_ENABLE_OFF = 0; + static int OPT_ENABLE_IGNORED = -1; + final OptionSpec offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel") .withRequiredArg(); @@ -106,11 +110,7 @@ public EditOfferOptionParser parse() { throw new IllegalArgumentException("no fixed price specified"); String fixedPriceAsString = options.valueOf(fixedPriceOpt); - try { - Double.valueOf(fixedPriceAsString); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException(format("%s is not a number", fixedPriceAsString)); - } + verifyStringIsValidDouble(fixedPriceAsString); boolean fixedPriceOptIsOnlyOpt = !options.has(mktPriceMarginOpt) && !options.has(triggerPriceOpt) @@ -137,11 +137,7 @@ public EditOfferOptionParser parse() { if (priceMarginAsString.isEmpty()) throw new IllegalArgumentException("no market price margin specified"); - try { - Double.valueOf(priceMarginAsString); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException(format("%s is not a number", priceMarginAsString)); - } + verifyStringIsValidDouble(priceMarginAsString); boolean mktPriceMarginOptIsOnlyOpt = !options.has(triggerPriceOpt) && !options.has(fixedPriceOpt) @@ -167,11 +163,7 @@ public EditOfferOptionParser parse() { if (triggerPriceAsString.isEmpty()) throw new IllegalArgumentException("trigger price not specified"); - try { - Double.valueOf(triggerPriceAsString); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException(format("%s is not a number", triggerPriceAsString)); - } + verifyStringIsValidDouble(triggerPriceAsString); boolean triggerPriceOptIsOnlyOpt = !options.has(mktPriceMarginOpt) && !options.has(fixedPriceOpt) @@ -214,11 +206,22 @@ public String getOfferId() { } public String getFixedPrice() { - return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0"; + if (offerEditType.equals(FIXED_PRICE_ONLY) || offerEditType.equals(FIXED_PRICE_AND_ACTIVATION_STATE)) { + return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0"; + } else { + return "0"; + } } public String getTriggerPrice() { - return options.has(triggerPriceOpt) ? options.valueOf(triggerPriceOpt) : "0"; + if (offerEditType.equals(TRIGGER_PRICE_ONLY) + || offerEditType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE) + || offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) + || offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE)) { + return options.has(triggerPriceOpt) ? options.valueOf(triggerPriceOpt) : "0"; + } else { + return "0"; + } } public BigDecimal getTriggerPriceAsBigDecimal() { @@ -226,17 +229,23 @@ public BigDecimal getTriggerPriceAsBigDecimal() { } public String getMktPriceMargin() { - return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00"; + if (offerEditType.equals(MKT_PRICE_MARGIN_ONLY) + || offerEditType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE) + || offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) + || offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE)) { + return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00"; + } else { + return "0.00"; + } } public BigDecimal getMktPriceMarginAsBigDecimal() { - return isUsingMktPriceMargin() - ? new BigDecimal(options.valueOf(mktPriceMarginOpt)) - : BigDecimal.ZERO; + return new BigDecimal(options.valueOf(mktPriceMarginOpt)); } public boolean isUsingMktPriceMargin() { - return options.has(mktPriceMarginOpt); + return !offerEditType.equals(FIXED_PRICE_ONLY) + && !offerEditType.equals(FIXED_PRICE_AND_ACTIVATION_STATE); } public int getEnableAsSignedInt() { @@ -247,8 +256,8 @@ public int getEnableAsSignedInt() { @Nullable Boolean input = isEnable(); return input == null - ? -1 - : input ? 1 : 0; + ? OPT_ENABLE_IGNORED + : input ? OPT_ENABLE_ON : OPT_ENABLE_OFF; } @Nullable @@ -261,4 +270,12 @@ public Boolean isEnable() { public EditOfferRequest.EditType getOfferEditType() { return offerEditType; } + + private void verifyStringIsValidDouble(String string) { + try { + Double.valueOf(string); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(format("%s is not a number", string)); + } + } } diff --git a/cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java b/cli/src/test/java/bisq/cli/opts/EditOfferOptionParserTest.java similarity index 87% rename from cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java rename to cli/src/test/java/bisq/cli/opts/EditOfferOptionParserTest.java index 3305b0cb2cf..ccabec4ea88 100644 --- a/cli/src/test/java/bisq/cli/opt/EditOfferOptionParserTest.java +++ b/cli/src/test/java/bisq/cli/opts/EditOfferOptionParserTest.java @@ -1,19 +1,19 @@ -package bisq.cli.opt; +package bisq.cli.opts; import org.junit.jupiter.api.Test; import static bisq.cli.Method.editoffer; +import static bisq.cli.opts.EditOfferOptionParser.OPT_ENABLE_IGNORED; +import static bisq.cli.opts.EditOfferOptionParser.OPT_ENABLE_OFF; +import static bisq.cli.opts.EditOfferOptionParser.OPT_ENABLE_ON; import static bisq.cli.opts.OptLabel.*; import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; - - -import bisq.cli.opts.EditOfferOptionParser; - -// This opt parser test has the most thorough coverage, +// This opt parser test ahs the most thorough coverage, // and is a reference for other opt parser tests. public class EditOfferOptionParserTest { @@ -124,7 +124,7 @@ public void testEditOfferActivationStateOnly() { }; EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(ACTIVATION_STATE_ONLY, parser.getOfferEditType()); - assertEquals(1, parser.getEnableAsSignedInt()); + assertEquals(OPT_ENABLE_ON, parser.getEnableAsSignedInt()); } @Test @@ -167,6 +167,9 @@ public void testEditOfferFixedPriceOnly() { EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(FIXED_PRICE_ONLY, parser.getOfferEditType()); assertEquals(fixedPriceAsString, parser.getFixedPrice()); + assertFalse(parser.isUsingMktPriceMargin()); + assertEquals("0.00", parser.getMktPriceMargin()); + assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt()); } @Test @@ -182,7 +185,9 @@ public void testEditOfferFixedPriceAndActivationStateOnly() { EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(FIXED_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType()); assertEquals(fixedPriceAsString, parser.getFixedPrice()); - assertEquals(0, parser.getEnableAsSignedInt()); + assertFalse(parser.isUsingMktPriceMargin()); + assertEquals("0.00", parser.getMktPriceMargin()); + assertEquals(OPT_ENABLE_OFF, parser.getEnableAsSignedInt()); } @Test @@ -196,7 +201,10 @@ public void testEditOfferMktPriceMarginOnly() { }; EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(MKT_PRICE_MARGIN_ONLY, parser.getOfferEditType()); + assertTrue(parser.isUsingMktPriceMargin()); assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + assertEquals("0", parser.getTriggerPrice()); + assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt()); } @Test @@ -225,8 +233,10 @@ public void testEditOfferMktPriceMarginAndActivationStateOnly() { }; EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE, parser.getOfferEditType()); + assertTrue(parser.isUsingMktPriceMargin()); assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); - assertEquals(0, parser.getEnableAsSignedInt()); + assertEquals("0", parser.getTriggerPrice()); + assertEquals(OPT_ENABLE_OFF, parser.getEnableAsSignedInt()); } @Test @@ -241,6 +251,9 @@ public void testEditTriggerPriceOnly() { EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(TRIGGER_PRICE_ONLY, parser.getOfferEditType()); assertEquals(triggerPriceAsString, parser.getTriggerPrice()); + assertTrue(parser.isUsingMktPriceMargin()); + assertEquals("0.00", parser.getMktPriceMargin()); + assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt()); } @Test @@ -284,7 +297,10 @@ public void testEditTriggerPriceAndActivationStateOnly() { EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(TRIGGER_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType()); assertEquals(triggerPriceAsString, parser.getTriggerPrice()); - assertEquals(1, parser.getEnableAsSignedInt()); + assertTrue(parser.isUsingMktPriceMargin()); + assertEquals("0.00", parser.getMktPriceMargin()); + assertEquals("0", parser.getFixedPrice()); + assertEquals(OPT_ENABLE_ON, parser.getEnableAsSignedInt()); } @Test @@ -300,8 +316,11 @@ public void testEditMKtPriceMarginAndTriggerPrice() { }; EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE, parser.getOfferEditType()); - assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); assertEquals(triggerPriceAsString, parser.getTriggerPrice()); + assertTrue(parser.isUsingMktPriceMargin()); + assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + assertEquals("0", parser.getFixedPrice()); + assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt()); } @Test @@ -318,8 +337,10 @@ public void testEditMKtPriceMarginAndTriggerPriceAndEnableState() { }; EditOfferOptionParser parser = new EditOfferOptionParser(args).parse(); assertEquals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType()); - assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); assertEquals(triggerPriceAsString, parser.getTriggerPrice()); - assertFalse(parser.isEnable()); + assertTrue(parser.isUsingMktPriceMargin()); + assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin()); + assertEquals("0", parser.getFixedPrice()); + assertEquals(OPT_ENABLE_OFF, parser.getEnableAsSignedInt()); } } diff --git a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java similarity index 97% rename from cli/src/test/java/bisq/cli/opt/OptionParsersTest.java rename to cli/src/test/java/bisq/cli/opts/OptionParsersTest.java index 1df62cf2aa8..58b8712fe90 100644 --- a/cli/src/test/java/bisq/cli/opt/OptionParsersTest.java +++ b/cli/src/test/java/bisq/cli/opts/OptionParsersTest.java @@ -1,4 +1,4 @@ -package bisq.cli.opt; +package bisq.cli.opts; import org.junit.jupiter.api.Test; @@ -11,13 +11,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; - -import bisq.cli.opts.CancelOfferOptionParser; -import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; -import bisq.cli.opts.CreateOfferOptionParser; -import bisq.cli.opts.CreatePaymentAcctOptionParser; - - public class OptionParsersTest { private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz"; From 063b52eb701ef312d55075e95817f8956a711402 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:39:06 -0300 Subject: [PATCH 19/40] Add editoffer test case, suppress annoying warnings --- .../apitest/method/offer/EditOfferTest.java | 51 +++++++++++++++---- .../java/bisq/apitest/scenario/OfferTest.java | 1 + .../cli/request/OffersServiceRequest.java | 3 ++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java index d7af5694af9..a7c661708ae 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java @@ -39,10 +39,7 @@ import static bisq.apitest.config.ApiTestConfig.BSQ; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; -import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_AND_ACTIVATION_STATE; -import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_AND_TRIGGER_PRICE; -import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE; -import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_ONLY; +import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -51,6 +48,7 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; +@SuppressWarnings("ALL") @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -175,6 +173,7 @@ public void testEditFixedPrice() { OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); var expectedNewFixedPrice = scaledUpFiatPrice.apply(new BigDecimal(editedFixedPriceAsString)); assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); + assertFalse(editedOffer.getUseMarketBasedPrice()); doSanityCheck(originalOffer, editedOffer); } @@ -211,6 +210,40 @@ public void testEditFixedPriceAndDeactivation() { @Test @Order(7) + public void testEditMktPriceMarginAndDeactivation() { + PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); + + var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); + OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), + DOLLAR, + paymentAcct.getId(), + originalMktPriceMargin, + 0); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + originalOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); + + // Edit the offer's price margin and trigger price, and deactivate it. + var newMktPriceMargin = new BigDecimal("1.50").doubleValue(); + aliceClient.editOffer(originalOffer.getId(), + "0.00", + originalOffer.getUseMarketBasedPrice(), + newMktPriceMargin, + 0, + DEACTIVATE_OFFER, + MKT_PRICE_MARGIN_AND_ACTIVATION_STATE); + // Wait for edited offer to be removed from offer-book, edited, and re-published. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + assertFalse(editedOffer.getIsActivated()); + + doSanityCheck(originalOffer, editedOffer); + } + + @Test + @Order(8) public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); @@ -249,7 +282,7 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { } @Test - @Order(8) + @Order(9) public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("US"); var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); @@ -279,7 +312,7 @@ public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException( } @Test - @Order(9) + @Order(10) public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); @@ -301,7 +334,7 @@ public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { } @Test - @Order(10) + @Order(11) public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("MX"); double mktPriceAsDouble = aliceClient.getBtcPrice("MXN"); @@ -335,7 +368,7 @@ public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { } @Test - @Order(11) + @Order(12) public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("GB"); double mktPriceAsDouble = aliceClient.getBtcPrice("GBP"); @@ -360,10 +393,10 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + assertEquals(scaledUpFiatPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice()); assertFalse(editedOffer.getUseMarketBasedPrice()); assertEquals(0.00, editedOffer.getMarketPriceMargin()); assertEquals(0, editedOffer.getTriggerPrice()); - assertEquals(scaledUpFiatPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice()); assertFalse(editedOffer.getIsActivated()); } diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java index 292df14a511..0c23c750ec6 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -98,6 +98,7 @@ public void testEditOffer() { test.testEditMktPriceMargin(); test.testEditFixedPrice(); test.testEditFixedPriceAndDeactivation(); + test.testEditMktPriceMarginAndDeactivation(); test.testEditMktPriceMarginAndTriggerPriceAndDeactivation(); test.testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException(); test.testEditingTriggerPriceInFixedPriceOfferShouldThrowException(); diff --git a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java index b340f25ea7a..c3cff777b1b 100644 --- a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java @@ -126,6 +126,7 @@ public OfferInfo createOffer(String direction, // TODO Make sure this is not duplicated anywhere on CLI side. private final Function scaledPriceStringFormat = (price) -> { BigDecimal factor = new BigDecimal(10).pow(4); + //noinspection BigDecimalMethodWithoutRoundingCalled return new BigDecimal(price).divide(factor).toPlainString(); }; @@ -195,6 +196,7 @@ public void editOffer(String offerId, .setEnable(enable) .setEditType(editType) .build(); + //noinspection ResultOfMethodCallIgnored grpcStubs.offersService.editOffer(request); } @@ -202,6 +204,7 @@ public void cancelOffer(String offerId) { var request = CancelOfferRequest.newBuilder() .setId(offerId) .build(); + //noinspection ResultOfMethodCallIgnored grpcStubs.offersService.cancelOffer(request); } From e5b5a06b9bc05e444532fa4903ed5f15a149d1a7 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 12:04:39 -0300 Subject: [PATCH 20/40] Remove unused field --- core/src/main/java/bisq/core/api/EditOfferValidator.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index 82f6b78b737..722c644ef95 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -22,7 +22,6 @@ class EditOfferValidator { private final EditOfferRequest.EditType editType; private final boolean isZeroEditedFixedPriceString; - private final boolean isZeroEditedMarketPriceMargin; private final boolean isZeroEditedTriggerPrice; EditOfferValidator(OpenOffer currentlyOpenOffer, @@ -41,7 +40,6 @@ class EditOfferValidator { this.editType = editType; this.isZeroEditedFixedPriceString = new BigDecimal(editedPriceAsString).doubleValue() == 0; - this.isZeroEditedMarketPriceMargin = editedMarketPriceMargin == 0; this.isZeroEditedTriggerPrice = editedTriggerPrice == 0; } @@ -76,7 +74,7 @@ private void validateEditedActivationState() { if (editedEnable < 0) throw new IllegalStateException( format("programmer error: the 'enable' request parameter does not" - + " indicate activation state of offer with id '{}' should be changed.", + + " indicate activation state of offer with id '%s' should be changed.", currentlyOpenOffer.getId())); } From bc1576efbca5ab814b710d9d7df8bc95e8eff52d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 13:13:52 -0300 Subject: [PATCH 21/40] Throw exception is edit altcoin offer is attempted Support for editing BSQ offers is in place, but will be added in another PR. --- .../apitest/method/offer/EditOfferTest.java | 20 +++++++++++++++++++ .../java/bisq/apitest/scenario/OfferTest.java | 2 ++ .../java/bisq/core/api/CoreOffersService.java | 9 ++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java index a7c661708ae..6cfa0241ca6 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java @@ -400,6 +400,26 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { assertFalse(editedOffer.getIsActivated()); } + @Test + @Order(13) + public void testEditBsqOfferShouldThrowException() { + createBsqPaymentAccounts(); + var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 100_000_000L, + 100_000_000L, + "0.00005", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + BSQ); + // TODO Allow editing BSQ offer fixed-price, enable/disable. + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.editOfferActivationState(newOffer.getId(), DEACTIVATE_OFFER)); + String expectedExceptionMessage = format("UNKNOWN: editing altcoin offer not supported"); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + private OfferInfo createMktPricedOfferForEdit(String direction, String currencyCode, String paymentAccountId, diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java index 0c23c750ec6..e37c56a8302 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -104,5 +104,7 @@ public void testEditOffer() { test.testEditingTriggerPriceInFixedPriceOfferShouldThrowException(); test.testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice(); test.testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt(); + // TODO Allow editing BSQ offer fixed-price, enable/disable. + test.testEditBsqOfferShouldThrowException(); } } diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index adcbc53f8c8..2a1aa09c721 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -17,6 +17,7 @@ package bisq.core.api; +import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.CreateOfferService; @@ -45,6 +46,7 @@ import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -215,6 +217,11 @@ void editOffer(String offerId, int editedEnable, EditType editType) { OpenOffer openOffer = getMyOpenOffer(offerId); + + boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(openOffer.getOffer().getCurrencyCode()); + if (isCryptoCurrency) + throw new IllegalStateException("editing altcoin offer not supported"); + new EditOfferValidator(openOffer, editedPriceAsString, editedUseMarketBasedPrice, @@ -310,7 +317,7 @@ private OfferPayload getMergedOfferPayload(OpenOffer openOffer, || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE) || editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE); MutableOfferPayloadFields mutableOfferPayloadFields = new MutableOfferPayloadFields( - editedPrice.getValue(), + Objects.requireNonNull(editedPrice).getValue(), isUsingMktPriceMargin ? exactMultiply(editedMarketPriceMargin, 0.01) : 0.00, isUsingMktPriceMargin, offer.getOfferPayload().getBaseCurrencyCode(), From a3ea4ecbf6c75a2cfae92910a8c05d29ace224c0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 13:22:07 -0300 Subject: [PATCH 22/40] Avoid duplicate test run --- .../method/offer/CreateOfferUsingMarketPriceMarginTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index df1f9079fb1..a34f144403e 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -31,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -53,7 +54,7 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; -// @Disabled +@Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest { From 3d38a8555f4587da4afc629306b261afa23c81d3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 15 Jun 2021 13:40:33 -0300 Subject: [PATCH 23/40] Make codacy just a bit happier --- core/src/main/java/bisq/core/api/CoreOffersService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 2a1aa09c721..234bc51b854 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -17,7 +17,6 @@ package bisq.core.api; -import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; import bisq.core.offer.CreateOfferService; @@ -218,7 +217,7 @@ void editOffer(String offerId, EditType editType) { OpenOffer openOffer = getMyOpenOffer(offerId); - boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(openOffer.getOffer().getCurrencyCode()); + boolean isCryptoCurrency = isCryptoCurrency(openOffer.getOffer().getCurrencyCode()); if (isCryptoCurrency) throw new IllegalStateException("editing altcoin offer not supported"); From 1a56a5161adabe4209058aad55f7c1531a53b1aa Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 17 Jun 2021 10:13:18 -0300 Subject: [PATCH 24/40] Force codacy check after codacy config change --- core/src/main/java/bisq/core/api/CoreOffersService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 234bc51b854..42eeb036f40 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -228,7 +228,7 @@ void editOffer(String offerId, editedTriggerPrice, editedEnable, editType).validate(); - log.info("'editoffer' params OK for offerId={}" + log.info("Validated 'editoffer' params offerId={}" + "\n\teditedPriceAsString={}" + "\n\teditedUseMarketBasedPrice={}" + "\n\teditedMarketPriceMargin={}" From 0e9c6650e3aca09947a91f6bb5222e638c7c1b90 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 18 Jun 2021 17:40:21 -0300 Subject: [PATCH 25/40] Include isMyOffer flag in API's trade/offer proto wrappers Optionally displaying an ENABLED column in CLI side getoffer output depends on the value of offer.isMyOffer, which is passed via new boolean arguments to the trade & offer pojo builders. --- .../src/main/java/bisq/core/api/model/OfferInfo.java | 12 +++++++++--- .../src/main/java/bisq/core/api/model/TradeInfo.java | 10 ++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/api/model/OfferInfo.java b/core/src/main/java/bisq/core/api/model/OfferInfo.java index ce645e6ab69..f99df264569 100644 --- a/core/src/main/java/bisq/core/api/model/OfferInfo.java +++ b/core/src/main/java/bisq/core/api/model/OfferInfo.java @@ -63,7 +63,7 @@ public class OfferInfo implements Payload { private final long date; private final String state; private final boolean isActivated; - private final boolean isMyOffer; + private boolean isMyOffer; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.id; @@ -93,13 +93,19 @@ public OfferInfo(OfferInfoBuilder builder) { this.isMyOffer = builder.isMyOffer; } + // Allow isMyOffer to be set on new offers' OfferInfo instances. + public void setIsMyOffer(boolean myOffer) { + isMyOffer = myOffer; + } + public static OfferInfo toOfferInfo(Offer offer) { - // Offer is not mine. + // Assume the offer is not mine, but isMyOffer can be reset to true, i.e., when + // calling TradeInfo toTradeInfo(Trade trade, String role, boolean isMyOffer); return getOfferInfoBuilder(offer, false).build(); } public static OfferInfo toOfferInfo(OpenOffer openOffer) { - // OpenOffer is mine. + // An OpenOffer is always my offer. return getOfferInfoBuilder(openOffer.getOffer(), true) .withTriggerPrice(openOffer.getTriggerPrice()) .withIsActivated(!openOffer.isDeactivated()) diff --git a/core/src/main/java/bisq/core/api/model/TradeInfo.java b/core/src/main/java/bisq/core/api/model/TradeInfo.java index 078e5ee4d9c..5779baf348e 100644 --- a/core/src/main/java/bisq/core/api/model/TradeInfo.java +++ b/core/src/main/java/bisq/core/api/model/TradeInfo.java @@ -92,11 +92,11 @@ public TradeInfo(TradeInfoBuilder builder) { this.contract = builder.contract; } - public static TradeInfo toTradeInfo(Trade trade) { - return toTradeInfo(trade, null); + public static TradeInfo toNewTradeInfo(Trade trade) { + return toTradeInfo(trade, null, false); } - public static TradeInfo toTradeInfo(Trade trade, String role) { + public static TradeInfo toTradeInfo(Trade trade, String role, boolean isMyOffer) { ContractInfo contractInfo; if (trade.getContract() != null) { Contract contract = trade.getContract(); @@ -116,8 +116,10 @@ public static TradeInfo toTradeInfo(Trade trade, String role) { contractInfo = ContractInfo.emptyContract.get(); } + OfferInfo offerInfo = toOfferInfo(trade.getOffer()); + offerInfo.setIsMyOffer(isMyOffer); return new TradeInfoBuilder() - .withOffer(toOfferInfo(trade.getOffer())) + .withOffer(offerInfo) .withTradeId(trade.getId()) .withShortId(trade.getShortId()) .withDate(trade.getDate().getTime()) From a603044f2eda650309646f3f0ab5e61be20945aa Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 18 Jun 2021 17:41:45 -0300 Subject: [PATCH 26/40] Pass isMyOffer flag to trade/offer proto wrappers from core services --- core/src/main/java/bisq/core/api/CoreApi.java | 4 ++++ .../main/java/bisq/core/api/CoreOffersService.java | 11 ++++++----- .../main/java/bisq/daemon/grpc/GrpcOffersService.java | 1 + .../main/java/bisq/daemon/grpc/GrpcTradesService.java | 6 ++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 1c223df2b7f..2fb5c39c5a0 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -179,6 +179,10 @@ public void cancelOffer(String id) { coreOffersService.cancelOffer(id); } + public boolean isMyOffer(String id) { + return coreOffersService.isMyOffer(id); + } + /////////////////////////////////////////////////////////////////////////////////////////// // PaymentAccounts /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 42eeb036f40..04ce9c2ceb3 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -155,6 +155,12 @@ OpenOffer getMyOpenOffer(String id) { new IllegalStateException(format("offer with id '%s' not found", id))); } + boolean isMyOffer(String id) { + return openOfferManager.getOpenOfferById(id) + .filter(open -> open.getOffer().isMyOffer(keyRing)) + .isPresent(); + } + // Create and place new offer. void createAndPlaceOffer(String currencyCode, String directionAsString, @@ -216,11 +222,6 @@ void editOffer(String offerId, int editedEnable, EditType editType) { OpenOffer openOffer = getMyOpenOffer(offerId); - - boolean isCryptoCurrency = isCryptoCurrency(openOffer.getOffer().getCurrencyCode()); - if (isCryptoCurrency) - throw new IllegalStateException("editing altcoin offer not supported"); - new EditOfferValidator(openOffer, editedPriceAsString, editedUseMarketBasedPrice, diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java index 2917bae6289..b1f9212d46b 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -161,6 +161,7 @@ public void createOffer(CreateOfferRequest req, // This result handling consumer's accept operation will return // the new offer to the gRPC client after async placement is done. OfferInfo offerInfo = toOfferInfo(offer); + offerInfo.setIsMyOffer(true); CreateOfferReply reply = CreateOfferReply.newBuilder() .setOffer(offerInfo.toProtoMessage()) .build(); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 8cfc74f7463..7ffea95e6a5 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -44,6 +44,7 @@ import lombok.extern.slf4j.Slf4j; +import static bisq.core.api.model.TradeInfo.toNewTradeInfo; import static bisq.core.api.model.TradeInfo.toTradeInfo; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static bisq.proto.grpc.TradesGrpc.*; @@ -72,9 +73,10 @@ public void getTrade(GetTradeRequest req, StreamObserver responseObserver) { try { Trade trade = coreApi.getTrade(req.getTradeId()); + boolean isMyOffer = coreApi.isMyOffer(trade.getOffer().getId()); String role = coreApi.getTradeRole(req.getTradeId()); var reply = GetTradeReply.newBuilder() - .setTrade(toTradeInfo(trade, role).toProtoMessage()) + .setTrade(toTradeInfo(trade, role, isMyOffer).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -99,7 +101,7 @@ public void takeOffer(TakeOfferRequest req, req.getPaymentAccountId(), req.getTakerFeeCurrencyCode(), trade -> { - TradeInfo tradeInfo = toTradeInfo(trade); + TradeInfo tradeInfo = toNewTradeInfo(trade); var reply = TakeOfferReply.newBuilder() .setTrade(tradeInfo.toProtoMessage()) .build(); From e32e0d1fbbcaa1bdc2ef4cee498541604ee3434a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 18 Jun 2021 17:46:16 -0300 Subject: [PATCH 27/40] Add altcoin (bsq) offer editing validation check BSQ offers are fixed-price only. This change blocks an attempt to change an altcoin offer to a margin price based offer, or set a trigger price. --- .../main/java/bisq/core/api/EditOfferValidator.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/src/main/java/bisq/core/api/EditOfferValidator.java b/core/src/main/java/bisq/core/api/EditOfferValidator.java index 722c644ef95..7a9840c3cdb 100644 --- a/core/src/main/java/bisq/core/api/EditOfferValidator.java +++ b/core/src/main/java/bisq/core/api/EditOfferValidator.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; +import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static java.lang.String.format; @Slf4j @@ -61,6 +62,7 @@ void validate() { case TRIGGER_PRICE_AND_ACTIVATION_STATE: case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE: case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE: { + checkNotAltcoinOffer(); validateEditedTriggerPrice(); validateEditedMarketPriceMargin(); break; @@ -128,4 +130,12 @@ private void validateEditedTriggerPrice() { + " in offer with id '%s'", currentlyOpenOffer.getId())); } + + private void checkNotAltcoinOffer() { + if (isCryptoCurrency(currentlyOpenOffer.getOffer().getCurrencyCode())) { + throw new IllegalStateException( + format("cannot set mkt price margin or trigger price on fixed price altcoin offer with id '%s'", + currentlyOpenOffer.getId())); + } + } } From b74f084893f1893fd7ccf7d4200ae19812f140f5 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 18 Jun 2021 17:50:28 -0300 Subject: [PATCH 28/40] Optionally show ENABLED column in CLI's getoffer(bsq) output --- cli/src/main/java/bisq/cli/TableFormat.java | 76 +++++++++++++++------ 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index d1d4aa729ac..3d14b5991d4 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -153,7 +153,7 @@ public static String formatOfferTable(List offers, String currencyCod boolean isMyOffer = offers.get(0).getIsMyOffer(); return baseCurrencyCode.equalsIgnoreCase("BTC") ? formatFiatOfferTable(offers, currencyCode, isMyOffer) - : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode); + : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode, isMyOffer); } private static String formatFiatOfferTable(List offers, @@ -252,14 +252,21 @@ private static String getFiatOfferColDataFormat(boolean isMyOffer, } } - private static String formatCryptoCurrencyOfferTable(List offers, String cryptoCurrencyCode) { + private static String formatCryptoCurrencyOfferTable(List offers, + String cryptoCurrencyCode, + boolean isMyOffer) { // Some column values might be longer than header, so we need to calculate them. int directionColWidth = getLongestDirectionColWidth(offers); int amountColWith = getLongestAmountColWidth(offers); int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers); int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); + // "Enabled" column is displayed for my offers only. + String enabledHeaderFormat = isMyOffer ? + COL_HEADER_ENABLED + COL_HEADER_DELIMITER + : ""; // TODO use memoize function to avoid duplicate the formatting done above? - String headersFormat = padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER + String headersFormat = enabledHeaderFormat + + padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER + COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER // COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode @@ -270,24 +277,51 @@ private static String formatCryptoCurrencyOfferTable(List offers, Str String headerLine = format(headersFormat, cryptoCurrencyCode.toUpperCase(), cryptoCurrencyCode.toUpperCase()); - String colDataFormat = "%-" + directionColWidth + "s" - + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s" - + " %" + amountColWith + "s" - + " %" + (volumeColWidth - 1) + "s" - + " %-" + paymentMethodColWidth + "s" - + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" - + " %-" + COL_HEADER_UUID.length() + "s"; - return headerLine - + offers.stream() - .map(o -> format(colDataFormat, - directionFormat.apply(o), - formatCryptoCurrencyPrice(o.getPrice()), - formatAmountRange(o.getMinAmount(), o.getAmount()), - formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()), - o.getPaymentMethodShortName(), - formatTimestamp(o.getDate()), - o.getId())) - .collect(Collectors.joining("\n")); + String colDataFormat; + if (isMyOffer) { + colDataFormat = "%-" + (COL_HEADER_ENABLED.length() + COL_HEADER_DELIMITER.length()) + "s" + + "%-" + directionColWidth + "s" + + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; + } else { + colDataFormat = "%-" + directionColWidth + "s" + + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s" + + " %" + amountColWith + "s" + + " %" + (volumeColWidth - 1) + "s" + + " %-" + paymentMethodColWidth + "s" + + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + + " %-" + COL_HEADER_UUID.length() + "s"; + } + if (isMyOffer) { + return headerLine + + offers.stream() + .map(o -> format(colDataFormat, + o.getIsActivated() ? "YES" : "NO", + directionFormat.apply(o), + formatCryptoCurrencyPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) + .collect(Collectors.joining("\n")); + } else { + return headerLine + + offers.stream() + .map(o -> format(colDataFormat, + directionFormat.apply(o), + formatCryptoCurrencyPrice(o.getPrice()), + formatAmountRange(o.getMinAmount(), o.getAmount()), + formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()), + o.getPaymentMethodShortName(), + formatTimestamp(o.getDate()), + o.getId())) + .collect(Collectors.joining("\n")); + } } private static int getLongestPaymentMethodColWidth(List offers) { From 7880a84a00d8e3a9739e464ee5710699872a28b3 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 18 Jun 2021 17:52:03 -0300 Subject: [PATCH 29/40] Add BSQ offer editing tests to EditOfferTest And log CLI's getoffer output to see getoffer formatting -- after adding new ENABLED and TRIGGER-PRICE columns. --- .../method/offer/AbstractOfferTest.java | 24 ++- .../apitest/method/offer/EditOfferTest.java | 202 ++++++++++++++++-- .../LongRunningOfferDeactivationTest.java | 4 +- .../java/bisq/apitest/scenario/OfferTest.java | 10 +- 4 files changed, 208 insertions(+), 32 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java index 81903f8efcc..494b249af5d 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -71,23 +71,31 @@ public static void setUp() { protected final Function scaledDownMktPriceMargin = (mktPriceMargin) -> exactMultiply(mktPriceMargin, 0.01); - // Price value of offer returned from server is scaled up by 10^4. - protected final Function scaledUpFiatPrice = (price) -> { + // Price value of fiat offer returned from server will be scaled up by 10^4. + protected final Function scaledUpFiatOfferPrice = (price) -> { BigDecimal factor = new BigDecimal(10).pow(4); return price.multiply(factor).longValue(); }; - protected final BiFunction calcTriggerPriceAsLong = (base, delta) -> { - var triggerPriceAsDouble = new BigDecimal(base).add(new BigDecimal(delta)).doubleValue(); - return Double.valueOf(exactMultiply(triggerPriceAsDouble, 10_000)).longValue(); + // Price value of altcoin offer returned from server will be scaled up by 10^8. + protected final Function scaledUpAltcoinOfferPrice = (altcoinPriceAsString) -> { + BigDecimal factor = new BigDecimal(10).pow(8); + BigDecimal priceAsBigDecimal = new BigDecimal(altcoinPriceAsString); + return priceAsBigDecimal.multiply(factor).longValue(); }; - protected final BiFunction calcFixedPriceAsString = (base, delta) -> { - var fixedPriceAsBigDecimal = new BigDecimal(Double.toString(base)) + protected final BiFunction calcPriceAsLong = (base, delta) -> { + var priceAsDouble = new BigDecimal(base).add(new BigDecimal(delta)).doubleValue(); + return Double.valueOf(exactMultiply(priceAsDouble, 10_000)).longValue(); + }; + + protected final BiFunction calcPriceAsString = (base, delta) -> { + var priceAsBigDecimal = new BigDecimal(Double.toString(base)) .add(new BigDecimal(Double.toString(delta))); - return fixedPriceAsBigDecimal.toPlainString(); + return priceAsBigDecimal.toPlainString(); }; + @SuppressWarnings("ConstantConditions") public static void createBsqPaymentAccounts() { alicesBsqAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's BSQ Account", BSQ, diff --git a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java index 6cfa0241ca6..a947044d07c 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/EditOfferTest.java @@ -38,9 +38,11 @@ import org.junit.jupiter.api.TestMethodOrder; import static bisq.apitest.config.ApiTestConfig.BSQ; +import static bisq.cli.TableFormat.formatOfferTable; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.proto.grpc.EditOfferRequest.EditType.*; import static java.lang.String.format; +import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -69,6 +71,7 @@ public void testOfferDisableAndEnable() { paymentAcct.getId(), 0.0, NO_TRIGGER_PRICE); + log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR")); assertFalse(originalOffer.getIsActivated()); // Not activated until prep is done. genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); @@ -77,11 +80,13 @@ public void testOfferDisableAndEnable() { aliceClient.editOfferActivationState(originalOffer.getId(), DEACTIVATE_OFFER); genBtcBlocksThenWait(1, 1500); // Wait for offer book removal. OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR")); assertFalse(editedOffer.getIsActivated()); // Re-enable offer aliceClient.editOfferActivationState(editedOffer.getId(), ACTIVATE_OFFER); genBtcBlocksThenWait(1, 1500); // Wait for offer book re-entry. editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR")); assertTrue(editedOffer.getIsActivated()); doSanityCheck(originalOffer, editedOffer); @@ -96,6 +101,7 @@ public void testEditTriggerPrice() { paymentAcct.getId(), 0.0, NO_TRIGGER_PRICE); + log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); assertEquals(0 /*no trigger price*/, originalOffer.getTriggerPrice()); @@ -103,11 +109,12 @@ public void testEditTriggerPrice() { // Edit the offer's trigger price, nothing else. var mktPrice = aliceClient.getBtcPrice("EUR"); var delta = 5_000.00; - var newTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPrice, delta); + var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPrice, delta); aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPriceAsLong); sleep(2500); // Wait for offer book re-entry. OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR")); assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); assertTrue(editedOffer.getUseMarketBasedPrice()); @@ -123,6 +130,7 @@ public void testSetTriggerPriceToNegativeValueShouldThrowException() { paymentAcct.getId(), 0.0, NO_TRIGGER_PRICE); + log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Edit the offer's trigger price, set to -1, check error. Throwable exception = assertThrows(StatusRuntimeException.class, () -> @@ -143,12 +151,14 @@ public void testEditMktPriceMargin() { paymentAcct.getId(), originalMktPriceMargin, NO_TRIGGER_PRICE); + log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); // Edit the offer's price margin, nothing else. var newMktPriceMargin = new BigDecimal("0.5").doubleValue(); aliceClient.editOfferPriceMargin(originalOffer.getId(), newMktPriceMargin); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD")); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); doSanityCheck(originalOffer, editedOffer); @@ -159,19 +169,21 @@ public void testEditMktPriceMargin() { public void testEditFixedPrice() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); - String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 200_000.0000); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), RUBLE, paymentAcct.getId(), fixedPriceAsString); + log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Edit the offer's fixed price, nothing else. - String editedFixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 100_000.0000); + String editedFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 100_000.0000); aliceClient.editOfferFixedPrice(originalOffer.getId(), editedFixedPriceAsString); // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - var expectedNewFixedPrice = scaledUpFiatPrice.apply(new BigDecimal(editedFixedPriceAsString)); + log.info("EDITED RUB OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "RUB")); + var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString)); assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); assertFalse(editedOffer.getUseMarketBasedPrice()); @@ -183,14 +195,15 @@ public void testEditFixedPrice() { public void testEditFixedPriceAndDeactivation() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); - String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 200_000.0000); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), RUBLE, paymentAcct.getId(), fixedPriceAsString); + log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Edit the offer's fixed price and deactivate it. - String editedFixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 100_000.0000); + String editedFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 100_000.0000); aliceClient.editOffer(originalOffer.getId(), editedFixedPriceAsString, originalOffer.getUseMarketBasedPrice(), @@ -201,7 +214,8 @@ public void testEditFixedPriceAndDeactivation() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - var expectedNewFixedPrice = scaledUpFiatPrice.apply(new BigDecimal(editedFixedPriceAsString)); + log.info("EDITED RUB OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "RUB")); + var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString)); assertEquals(expectedNewFixedPrice, editedOffer.getPrice()); assertFalse(editedOffer.getIsActivated()); @@ -219,6 +233,7 @@ public void testEditMktPriceMarginAndDeactivation() { paymentAcct.getId(), originalMktPriceMargin, 0); + log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); @@ -235,6 +250,7 @@ public void testEditMktPriceMarginAndDeactivation() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD")); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(0, editedOffer.getTriggerPrice()); assertFalse(editedOffer.getIsActivated()); @@ -249,13 +265,14 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { var originalMktPriceMargin = new BigDecimal("0.0").doubleValue(); var mktPriceAsDouble = aliceClient.getBtcPrice(DOLLAR); - var originalTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); + var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -5_000.0000); OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), DOLLAR, paymentAcct.getId(), originalMktPriceMargin, originalTriggerPriceAsLong); + log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. originalOffer = aliceClient.getMyOffer(originalOffer.getId()); assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin()); @@ -263,7 +280,7 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { // Edit the offer's price margin and trigger price, and deactivate it. var newMktPriceMargin = new BigDecimal("0.1").doubleValue(); - var newTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, -2_000.0000); + var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -2_000.0000); aliceClient.editOffer(originalOffer.getId(), "0.00", originalOffer.getUseMarketBasedPrice(), @@ -274,6 +291,7 @@ public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD")); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); assertFalse(editedOffer.getIsActivated()); @@ -291,6 +309,7 @@ public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException( paymentAcct.getId(), originalMktPriceMargin, NO_TRIGGER_PRICE); + log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Try to edit both the fixed price and mkt price margin. var newMktPriceMargin = new BigDecimal("0.25").doubleValue(); @@ -316,11 +335,12 @@ public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException( public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU"); double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE); - String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 200_000.0000); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), RUBLE, paymentAcct.getId(), fixedPriceAsString); + log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. long newTriggerPrice = 1000000L; Throwable exception = assertThrows(StatusRuntimeException.class, () -> @@ -338,17 +358,18 @@ public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() { public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { PaymentAccount paymentAcct = getOrCreatePaymentAccount("MX"); double mktPriceAsDouble = aliceClient.getBtcPrice("MXN"); - String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 0.00); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00); OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(), "MXN", paymentAcct.getId(), fixedPriceAsString); + log.info("ORIGINAL MXN OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "MXN")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. // Change the offer to mkt price based and set a trigger price. var newMktPriceMargin = new BigDecimal("0.05").doubleValue(); var delta = 200_000.0000; // trigger price on buy offer is 200K above mkt price - var newTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, delta); + var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, delta); aliceClient.editOffer(originalOffer.getId(), "0.00", true, @@ -359,6 +380,7 @@ public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED MXN OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "MXN")); assertTrue(editedOffer.getUseMarketBasedPrice()); assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin()); assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice()); @@ -374,15 +396,16 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { double mktPriceAsDouble = aliceClient.getBtcPrice("GBP"); var originalMktPriceMargin = new BigDecimal("0.25").doubleValue(); var delta = 1_000.0000; // trigger price on sell offer is 1K below mkt price - var originalTriggerPriceAsLong = calcTriggerPriceAsLong.apply(mktPriceAsDouble, delta); + var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, delta); final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(), "GBP", paymentAcct.getId(), originalMktPriceMargin, originalTriggerPriceAsLong); + log.info("ORIGINAL GBP OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "GBP")); genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. - String fixedPriceAsString = calcFixedPriceAsString.apply(mktPriceAsDouble, 0.00); + String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00); aliceClient.editOffer(originalOffer.getId(), fixedPriceAsString, false, @@ -393,7 +416,8 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { // Wait for edited offer to be removed from offer-book, edited, and re-published. genBtcBlocksThenWait(1, 2500); OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); - assertEquals(scaledUpFiatPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice()); + log.info("EDITED GBP OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "GBP")); + assertEquals(scaledUpFiatOfferPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice()); assertFalse(editedOffer.getUseMarketBasedPrice()); assertEquals(0.00, editedOffer.getMarketPriceMargin()); assertEquals(0, editedOffer.getTriggerPrice()); @@ -402,9 +426,9 @@ public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() { @Test @Order(13) - public void testEditBsqOfferShouldThrowException() { + public void testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowException() { createBsqPaymentAccounts(); - var newOffer = aliceClient.createFixedPricedOffer(BUY.name(), + OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), BSQ, 100_000_000L, 100_000_000L, @@ -412,13 +436,151 @@ public void testEditBsqOfferShouldThrowException() { getDefaultBuyerSecurityDepositAsPercent(), alicesBsqAcct.getId(), BSQ); - // TODO Allow editing BSQ offer fixed-price, enable/disable. + log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. Throwable exception = assertThrows(StatusRuntimeException.class, () -> - aliceClient.editOfferActivationState(newOffer.getId(), DEACTIVATE_OFFER)); - String expectedExceptionMessage = format("UNKNOWN: editing altcoin offer not supported"); + aliceClient.editOffer(originalOffer.getId(), + "0.00", + true, + 0.1, + 0, + ACTIVATE_OFFER, + MKT_PRICE_MARGIN_ONLY)); + String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or" + + " trigger price on fixed price altcoin offer with id '%s'", + originalOffer.getId()); assertEquals(expectedExceptionMessage, exception.getMessage()); } + @Test + @Order(14) + public void testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException() { + createBsqPaymentAccounts(); + OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 100_000_000L, + 100_000_000L, + "0.00005", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + BSQ); + log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + var newTriggerPriceAsLong = calcPriceAsLong.apply(0.00005, 0.00); + Throwable exception = assertThrows(StatusRuntimeException.class, () -> + aliceClient.editOffer(originalOffer.getId(), + "0.00", + false, + 0.1, + newTriggerPriceAsLong, + ACTIVATE_OFFER, + TRIGGER_PRICE_ONLY)); + String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or" + + " trigger price on fixed price altcoin offer with id '%s'", + originalOffer.getId()); + assertEquals(expectedExceptionMessage, exception.getMessage()); + } + + @Test + @Order(15) + public void testEditFixedPriceOnBsqOffer() { + createBsqPaymentAccounts(); + String fixedPriceAsString = "0.00005"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 100_000_000L, + 100_000_000L, + fixedPriceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + BSQ); + log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + String newFixedPriceAsString = "0.00003111"; + aliceClient.editOffer(originalOffer.getId(), + newFixedPriceAsString, + false, + 0.0, + 0, + ACTIVATE_OFFER, + FIXED_PRICE_ONLY); + // Wait for edited offer to be edited and removed from offer-book. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ)); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertTrue(editedOffer.getIsActivated()); + assertFalse(editedOffer.getUseMarketBasedPrice()); + assertEquals(0.00, editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + } + + @Test + @Order(16) + public void testDisableBsqOffer() { + createBsqPaymentAccounts(); + String fixedPriceAsString = "0.00005"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 100_000_000L, + 100_000_000L, + fixedPriceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + BSQ); + log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + aliceClient.editOffer(originalOffer.getId(), + fixedPriceAsString, + false, + 0.0, + 0, + DEACTIVATE_OFFER, + ACTIVATION_STATE_ONLY); + // Wait for edited offer to be removed from offer-book. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ)); + assertFalse(editedOffer.getIsActivated()); + assertEquals(scaledUpAltcoinOfferPrice.apply(fixedPriceAsString), editedOffer.getPrice()); + assertFalse(editedOffer.getUseMarketBasedPrice()); + assertEquals(0.00, editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + } + + @Test + @Order(17) + public void testEditFixedPriceAndDisableBsqOffer() { + createBsqPaymentAccounts(); + String fixedPriceAsString = "0.00005"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ + final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(), + BSQ, + 100_000_000L, + 100_000_000L, + fixedPriceAsString, + getDefaultBuyerSecurityDepositAsPercent(), + alicesBsqAcct.getId(), + BSQ); + log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ")); + genBtcBlocksThenWait(1, 2500); // Wait for offer book entry. + String newFixedPriceAsString = "0.000045"; + aliceClient.editOffer(originalOffer.getId(), + newFixedPriceAsString, + false, + 0.0, + 0, + DEACTIVATE_OFFER, + FIXED_PRICE_AND_ACTIVATION_STATE); + // Wait for edited offer to be edited and removed from offer-book. + genBtcBlocksThenWait(1, 2500); + OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId()); + log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ)); + assertFalse(editedOffer.getIsActivated()); + assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice()); + assertFalse(editedOffer.getUseMarketBasedPrice()); + assertEquals(0.00, editedOffer.getMarketPriceMargin()); + assertEquals(0, editedOffer.getTriggerPrice()); + } private OfferInfo createMktPricedOfferForEdit(String direction, String currencyCode, diff --git a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java index 2aeea5a436c..e7f09247a86 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/LongRunningOfferDeactivationTest.java @@ -59,7 +59,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest { public void testSellOfferAutoDisable(final TestInfo testInfo) { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); - long triggerPrice = calcTriggerPriceAsLong.apply(mktPriceAsDouble, -50.0000); + long triggerPrice = calcPriceAsLong.apply(mktPriceAsDouble, -50.0000); log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice)); OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(), "USD", @@ -107,7 +107,7 @@ public void testSellOfferAutoDisable(final TestInfo testInfo) { public void testBuyOfferAutoDisable(final TestInfo testInfo) { PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US"); double mktPriceAsDouble = aliceClient.getBtcPrice("USD"); - long triggerPrice = calcTriggerPriceAsLong.apply(mktPriceAsDouble, 50.0000); + long triggerPrice = calcPriceAsLong.apply(mktPriceAsDouble, 50.0000); log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice)); OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(), "USD", diff --git a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java index e37c56a8302..41ac197f1b5 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java +++ b/apitest/src/test/java/bisq/apitest/scenario/OfferTest.java @@ -92,6 +92,7 @@ public void testCreateBSQOffers() { @Order(6) public void testEditOffer() { EditOfferTest test = new EditOfferTest(); + // Edit fiat offer tests test.testOfferDisableAndEnable(); test.testEditTriggerPrice(); test.testSetTriggerPriceToNegativeValueShouldThrowException(); @@ -104,7 +105,12 @@ public void testEditOffer() { test.testEditingTriggerPriceInFixedPriceOfferShouldThrowException(); test.testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice(); test.testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt(); - // TODO Allow editing BSQ offer fixed-price, enable/disable. - test.testEditBsqOfferShouldThrowException(); + test.testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice(); + // Edit bsq offer tests + test.testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowException(); + test.testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException(); + test.testEditFixedPriceOnBsqOffer(); + test.testDisableBsqOffer(); + test.testEditFixedPriceAndDisableBsqOffer(); } } From acbf1e4323c5bf12615a14ad1b4b3017a69a9b7e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 19 Jun 2021 11:40:21 -0300 Subject: [PATCH 30/40] Force rebuild after github action ECONNRESET From 9703b87379ef179b7b47897a123efddaeef4767b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 19 Jun 2021 13:27:02 -0300 Subject: [PATCH 31/40] Document api 'editoffer' usage --- apitest/docs/api-beta-test-guide.md | 114 +++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/apitest/docs/api-beta-test-guide.md b/apitest/docs/api-beta-test-guide.md index 4a110a303ac..ff86acd54ac 100644 --- a/apitest/docs/api-beta-test-guide.md +++ b/apitest/docs/api-beta-test-guide.md @@ -408,8 +408,118 @@ The offer will be removed from other Bisq users' offer views, and paid transacti ### Editing an Existing Offer -Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees -for the canceled offer will be forfeited. +Offers you create can be edited in various ways: + +- Disable or re-enable an offer. +- Change an offer's price model and disable (or re-enable) it. +- Change a market price margin based offer to a fixed price offer. +- Change a market price margin based offer's price margin. +- Change, set, or remove a trigger price on a market price margin based offer. +- Change a market price margin based offer's price margin and trigger price. +- Change a market price margin based offer's price margin and remove its trigger price. +- Change a fixed price offer to a market price margin based offer. +- Change a fixed price offer's fixed price. + +_Note: the API does not support editing an offer's payment account._ + +The subsections below contain examples related to specific use cases. + +#### Enable and Disable Offer + +Existing offers you create can be disabled (removed from offer book) and re-enabled (re-published to offer book). + +To disable an offer: +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --enabled=false +``` + +To enable an offer: +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --enabled=true +``` + +#### Change Offer Pricing Model +The `editoffer` command can be used to change an existing market price margin based offer to a fixed price offer, +and vice-versa. + +##### Change Market Price Margin Based to Fixed Price Offer +Suppose you used `createoffer` to create a market price margin based offer as follows: +``` +$ ./bisq-cli --password=xyz --port=9998 createoffer \ + --payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \ + --direction=SELL \ + --currency-code=JPY \ + --amount=0.125 \ + --market-price-margin=0.5 \ + --security-deposit=15.0 \ + --fee-currency=BSQ +``` +To change the market price margin based offer to a fixed price offer: +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --fixed-price=3960000.5555 +``` + +##### Change Fixed Price Offer to Market Price Margin Based Offer +Suppose you used `createoffer` to create a fixed price offer as follows: +``` +$ ./bisq-cli --password=xyz --port=9998 createoffer \ + --payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \ + --direction=SELL \ + --currency-code=JPY \ + --amount=0.125 \ + --fixed-price=3960000.0000 \ + --security-deposit=15.0 \ + --fee-currency=BSQ +``` +To change the fixed price offer to a market price margin based offer: +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.5 +``` +Alternatively, you can also set a trigger price on the re-published, market price margin based offer. +A trigger price on a SELL offer causes the offer to be automatically disabled when the market price +falls below the trigger price. In the `editoffer` example below, the SELL offer will be disabled when +the JPY market price falls below 3960000.0000. + +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.5 \ + --trigger-price=3960000.0000 +``` +On a BUY offer, a trigger price causes the BUY offer to be automatically disabled when the market price +rises above the trigger price. + +_Note: Disabled offers never automatically re-enable; they can only be manually re-enabled via +`editoffer --offer-id= --enable=true`._ + +#### Remove Trigger Price +To remove a trigger price on a market price margin based offer, set the trigger price to 0: +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --trigger-price=0 +``` + +#### Change Pricing Model And Disable Offer +You can use `editoffer` to simultaneously change an offer's price details and disable or re-enable it. + +Suppose you have a disabled, fixed price offer, and want to change it to a market price margin based offer, set +a trigger price, and re-enable it: +``` +./bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.5 \ + --trigger-price=3960000.0000 \ + --enable=true +``` ### Taking Offers From 05f4f4dd8050682653179f69f0fcbb302198b91e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 19 Jun 2021 13:35:40 -0300 Subject: [PATCH 32/40] Fix header --- apitest/docs/api-beta-test-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apitest/docs/api-beta-test-guide.md b/apitest/docs/api-beta-test-guide.md index ff86acd54ac..6cab5f4eae5 100644 --- a/apitest/docs/api-beta-test-guide.md +++ b/apitest/docs/api-beta-test-guide.md @@ -508,7 +508,7 @@ To remove a trigger price on a market price margin based offer, set the trigger --trigger-price=0 ``` -#### Change Pricing Model And Disable Offer +#### Change Disabled Offer's Pricing Model and Enable It You can use `editoffer` to simultaneously change an offer's price details and disable or re-enable it. Suppose you have a disabled, fixed price offer, and want to change it to a market price margin based offer, set From 06efcdfcb94b1fd60383bec02f7c6a75f169558f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 11 Jul 2021 12:08:40 -0300 Subject: [PATCH 33/40] Delete tmp main() method --- .../offer/CreateOfferUsingMarketPriceMarginTest.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index a34f144403e..25052057dac 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -54,6 +54,7 @@ import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; +@SuppressWarnings("ConstantConditions") @Disabled @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -258,16 +259,6 @@ public void testCreateUSDBTCBuyOfferWithTriggerPrice() { assertEquals(triggerPriceAsLong, newOffer.getTriggerPrice()); } - public static void main(String[] args) { - // TODO DELETE ME - String triggerPriceAsString = "10.1111"; - Price price = Price.parse("USD", triggerPriceAsString); - long triggerPriceAsLong = price.getValue(); - log.info("triggerPriceAsString: {}", triggerPriceAsString); - log.info("triggerPriceAsPrice: {}", price); - log.info("triggerPriceAsLong: {}", triggerPriceAsLong); - } - private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) { assertTrue(() -> { String counterCurrencyCode = offer.getCounterCurrencyCode(); From eb62f9354af3fdbc0fc4fb0074fea81ecf0667dd Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:28:33 -0300 Subject: [PATCH 34/40] Rename and move private function And make sure function is not duplicated CLI side logic. --- .../bisq/cli/request/OffersServiceRequest.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java index c3cff777b1b..215c4f3e80d 100644 --- a/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java +++ b/cli/src/main/java/bisq/cli/request/OffersServiceRequest.java @@ -47,6 +47,12 @@ public class OffersServiceRequest { + private final Function scaledPriceStringRequestFormat = (price) -> { + BigDecimal factor = new BigDecimal(10).pow(4); + //noinspection BigDecimalMethodWithoutRoundingCalled + return new BigDecimal(price).divide(factor).toPlainString(); + }; + private final GrpcStubs grpcStubs; public OffersServiceRequest(GrpcStubs grpcStubs) { @@ -123,18 +129,11 @@ public OfferInfo createOffer(String direction, return grpcStubs.offersService.createOffer(request).getOffer(); } - // TODO Make sure this is not duplicated anywhere on CLI side. - private final Function scaledPriceStringFormat = (price) -> { - BigDecimal factor = new BigDecimal(10).pow(4); - //noinspection BigDecimalMethodWithoutRoundingCalled - return new BigDecimal(price).divide(factor).toPlainString(); - }; - public void editOfferActivationState(String offerId, int enable) { var offer = getMyOffer(offerId); var scaledPriceString = offer.getUseMarketBasedPrice() ? "0.00" - : scaledPriceStringFormat.apply(offer.getPrice()); + : scaledPriceStringRequestFormat.apply(offer.getPrice()); editOffer(offerId, scaledPriceString, offer.getUseMarketBasedPrice(), From 1992bcb1c0c08fba3bf24a21e4565f884ec379c4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:34:37 -0300 Subject: [PATCH 35/40] Do not duplicate Price.parse on CLI side for only one use case --- .../method/offer/CreateOfferUsingMarketPriceMarginTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index 25052057dac..451bd1f2c18 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -241,7 +241,6 @@ public void testCreateUSDBTCBuyOfferWithTriggerPrice() { double mktPriceAsDouble = aliceClient.getBtcPrice("usd"); BigDecimal mktPrice = new BigDecimal(Double.toString(mktPriceAsDouble)); BigDecimal triggerPrice = mktPrice.add(new BigDecimal("1000.9999")); - // TODO Duplicate this Price class logic in CLI. long triggerPriceAsLong = Price.parse("USD", triggerPrice.toString()).getValue(); var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(), From 622f7e9adda51df60db0910ce79cf8898f633834 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:39:53 -0300 Subject: [PATCH 36/40] Remove old TODO because relevant refactoring was approved --- .../src/test/java/bisq/apitest/scenario/bot/BotClient.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java index d6941a0a402..062ee742b19 100644 --- a/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java +++ b/apitest/src/test/java/bisq/apitest/scenario/bot/BotClient.java @@ -39,11 +39,6 @@ /** * Convenience GrpcClient wrapper for bots using gRPC services. - * - * TODO Consider if the duplication smell is bad enough to force a BotClient user - * to use the GrpcClient instead (and delete this class). But right now, I think it is - * OK because moving some of the non-gRPC related methods to GrpcClient is even smellier. - * */ @SuppressWarnings({"JavaDoc", "unused"}) @Slf4j From a4278a4147c78ae7e4350058457af59a0083c9de Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 13 Jul 2021 10:32:35 -0300 Subject: [PATCH 37/40] Fix typo 'enabled' -> 'enable' --- apitest/docs/api-beta-test-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apitest/docs/api-beta-test-guide.md b/apitest/docs/api-beta-test-guide.md index 6cab5f4eae5..8c1b41112bc 100644 --- a/apitest/docs/api-beta-test-guide.md +++ b/apitest/docs/api-beta-test-guide.md @@ -432,14 +432,14 @@ To disable an offer: ``` ./bisq-cli --password=xyz --port=9998 editoffer \ --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ - --enabled=false + --enable=false ``` To enable an offer: ``` ./bisq-cli --password=xyz --port=9998 editoffer \ --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ - --enabled=true + --enable=true ``` #### Change Offer Pricing Model From 649c98a3f0ba2916e0fde21ab70be7ff8ba9d60b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Thu, 15 Jul 2021 12:43:29 -0300 Subject: [PATCH 38/40] Always use Locale.US in CLI DecimalFormats Avoid inconsistent CLI output decimal formats across different systems' default locales. --- cli/src/main/java/bisq/cli/CurrencyFormat.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cli/src/main/java/bisq/cli/CurrencyFormat.java b/cli/src/main/java/bisq/cli/CurrencyFormat.java index 29639ec7b83..8d8a3d11fde 100644 --- a/cli/src/main/java/bisq/cli/CurrencyFormat.java +++ b/cli/src/main/java/bisq/cli/CurrencyFormat.java @@ -22,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.math.BigDecimal; @@ -35,6 +36,9 @@ @VisibleForTesting public class CurrencyFormat { + // Use the US locale for all DecimalFormat objects. + private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); + // Formats numbers in US locale, human friendly style. private static final NumberFormat FRIENDLY_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); @@ -42,12 +46,12 @@ public class CurrencyFormat { private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000"); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); - static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); - static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0"); + static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS); + static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0", DECIMAL_FORMAT_SYMBOLS); static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); - static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00"); - static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00"); + static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00", DECIMAL_FORMAT_SYMBOLS); + static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00", DECIMAL_FORMAT_SYMBOLS); static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01"); @@ -62,7 +66,6 @@ public static String formatBsq(long sats) { } public static String formatBsqAmount(long bsqSats) { - // BSQ sats = trade.getOffer().getVolume() FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(2); FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(2); FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP); From 1d21cb9c10882238c29153de049c33cf17b64d9a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 18 Jul 2021 17:49:04 -0300 Subject: [PATCH 39/40] Add missing 0 trigger-price param --- .../method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java index 3868fffc300..707df3bb40e 100644 --- a/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java +++ b/apitest/src/test/java/bisq/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java @@ -103,7 +103,8 @@ public void testTakeAlicesBuyOffer(final TestInfo testInfo) { 0.00, getDefaultBuyerSecurityDepositAsPercent(), alicesPaymentAccount.getId(), - TRADE_FEE_CURRENCY_CODE); + TRADE_FEE_CURRENCY_CODE, + 0L); var offerId = alicesOffer.getId(); assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); From 92365720a5adf7bc39f6d20180f21e83bc0e87e9 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 18 Jul 2021 18:19:25 -0300 Subject: [PATCH 40/40] Workaround API editoffer event rcv order problem in peer UI When the API is used to edit an offer, the daemon's P2PDataStorage will send onRemoved(offer) and onAdded(offer) events 1-3 ms apart, and those events may arrive at a peer's UI in the wrong order: onAdded, onRemoved. When the order is backwards, the edited offer disappears from the UI's OfferBook view. Reloading the offerBookListItems works around the problem. Reloading 100 offers usually takes less then 1 ms. I would prefer to solve this in 2+ other classes: - P2PDataStorage, where these events are fired in the correct order, but down to 1ms apart. - OfferBookService constructor, where the peer UI's HashMapChangedListener methods 'onRemoved' and 'onAdded' are causing API edited offers to dissappear if received in backwards order: 'onAdded', then 'onRemoved'. Trying to insert a delay between remove an offer for edit, then republishing the offer, does not work. The events can still arrive in the wrong order. I am going to need some help from @chimp1984 to solve it. --- .../bisq/desktop/main/offer/offerbook/OfferBook.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java index 753506bce12..8bcc9d782c1 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java @@ -120,7 +120,16 @@ public void removeOffer(Offer offer, TradeManager tradeManager) { Optional candidateToRemove = offerBookListItems.stream() .filter(item -> item.getOffer().getId().equals(offer.getId())) .findAny(); - candidateToRemove.ifPresent(offerBookListItems::remove); + candidateToRemove.ifPresent((item) -> { + offerBookListItems.remove(item); + // When the API is used to edit an offer, the daemon's P2PDataStorage will + // send onRemoved(offer) and onAdded(offer) events 1-3 ms apart, and those + // events may arrive at a peer's UI in the wrong order (onAdded, onRemoved), + // resulting in an edited offer disappearing from the UI's OfferBook. + // Reloading the offerBookListItems works around the problem. Reloading + // 100 offers usually takes less then 1 ms. + fillOfferBookListItems(); + }); } public ObservableList getOfferBookListItems() {