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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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 b4ee6dbc12cfde96abf2060f129d2db64e75326f Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 30 Jul 2021 11:58:12 -0300 Subject: [PATCH 39/56] Do not start test harness deamons in dbg mode by default --- .../test/java/bisq/apitest/method/offer/AbstractOfferTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 494b249af5d..2d2e5fc6d73 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/AbstractOfferTest.java @@ -58,7 +58,7 @@ public abstract class AbstractOfferTest extends MethodTest { @BeforeAll public static void setUp() { startSupportingApps(true, - true, + false, bitcoind, seednode, arbdaemon, From 95bbb41e51a36d13ca1b21b8ede82c9fd659ece4 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 30 Jul 2021 12:02:18 -0300 Subject: [PATCH 40/56] Add missing 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..1035875010e 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, + NO_TRIGGER_PRICE); var offerId = alicesOffer.getId(); assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); From add6536402daa36330711d6b73ad99157e00187e Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 30 Jul 2021 17:01:25 -0300 Subject: [PATCH 41/56] Fix peer add(offer) & remove(offer) event order problem Use LinkedHashSet to maintain NetworkEnvelope ordering when Connection#onBundleOfEnvelopes calls listeners. Connection#onBundleOfEnvelopes builds a set from an ordered list of NetworkEnvelopes, then calls listeners during set iteration. The envelope list ordering is lost if a HashSet is built, but maintained by switching to a LinkedHashSet. Losing the envelope ordering becomes a problem if the peer receives a RemoveDataMessage and an AddDataMessage in the same batch of envelopes, and relays them to listeners in the wrong (random) order. For example, an API 'editoffer' call may result in the edited offer being added, then immediately remove from their UI's offer book. --- p2p/src/main/java/bisq/network/p2p/network/Connection.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index bedef3ed9ea..e6bffbe1643 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -67,6 +67,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -439,7 +440,7 @@ public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) { Map> itemsByHash = new HashMap<>(); - Set envelopesToProcess = new HashSet<>(); + Set envelopesToProcess = new LinkedHashSet<>(); List networkEnvelopes = bundleOfEnvelopes.getEnvelopes(); for (NetworkEnvelope networkEnvelope : networkEnvelopes) { // If SendersNodeAddressMessage we do some verifications and apply if successful, otherwise we return false. From 094bc52552acd0cb2dc9d8ee981c93db2d0cbc22 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 2 Aug 2021 09:52:42 -0300 Subject: [PATCH 42/56] Revert "Fix peer add(offer) & remove(offer) event order problem" This reverts commit add6536402daa36330711d6b73ad99157e00187e. --- p2p/src/main/java/bisq/network/p2p/network/Connection.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/p2p/src/main/java/bisq/network/p2p/network/Connection.java b/p2p/src/main/java/bisq/network/p2p/network/Connection.java index e6bffbe1643..bedef3ed9ea 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/Connection.java +++ b/p2p/src/main/java/bisq/network/p2p/network/Connection.java @@ -67,7 +67,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -440,7 +439,7 @@ public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) { Map> itemsByHash = new HashMap<>(); - Set envelopesToProcess = new LinkedHashSet<>(); + Set envelopesToProcess = new HashSet<>(); List networkEnvelopes = bundleOfEnvelopes.getEnvelopes(); for (NetworkEnvelope networkEnvelope : networkEnvelopes) { // If SendersNodeAddressMessage we do some verifications and apply if successful, otherwise we return false. From 6e2400fb0fe8496a3934598f4e13af9908fb67ba Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 7 Aug 2021 14:06:38 -0300 Subject: [PATCH 43/56] Ensure UI OfferBook items are correctly added and removed Using the API's CLI to edit offers can sometimes result in add/remove messages being received on peers in the same batch of envolopes, and these messages are sometimes passed to the UI in (1) add, (2) remove order. This can result in a newly edited offer being removed immediately after being added to the OfferBook list. This change uses storage entry sequence number and storage entry payload hash comparisons to avoid the problem. - OfferBookListItem Added new constructor taking P2PDataStorage.ByteArray hashOfPayload, and int sequenceNumber params. Added a new toString() method. - OfferBook Added new checks on OfferBookListItem hashOfPayload and sequenceNumber while determining if offer candidates should be added or removed from the UI's OfferBook List. See OfferBook contructor's implementation of OfferBookChangedListener#onAdded and OfferBookChangedListener#onRemoved. Added many comments explaining the add/remove rules, and plenty of debug statements to help trace the add/remove event process. - OfferBookService#OfferBookChangedListener Added new P2PDataStorage.ByteArray hashOfPayload, and int sequenceNumber params to listener's onAdded and onRemoved method signatures. Added these two new paramater values to listener.onAdded and listener.onRemoved calls. - TakeOfferDataModel Replaced unused, old tradeManager param in offerBook.removeOffer() with (null) P2PDataStorage.ByteArray hashOfPayload, and (-1) int sequenceNumber params. OfferBook will remove the candidate offer as before. - MarketAlerts Adjusted onAdded() & onRemoved listener method signatures, even though new P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber params are not used by the implementations. --- .../alerts/market/MarketAlerts.java | 6 +- .../bisq/core/offer/OfferBookService.java | 22 ++- .../main/offer/offerbook/OfferBook.java | 165 +++++++++++++++--- .../offer/offerbook/OfferBookListItem.java | 45 ++++- .../offer/takeoffer/TakeOfferDataModel.java | 2 +- desktop/src/main/resources/logback.xml | 7 +- 6 files changed, 203 insertions(+), 44 deletions(-) diff --git a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java index bd170c08792..b4df5082114 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java +++ b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java @@ -32,6 +32,8 @@ import bisq.core.user.User; import bisq.core.util.FormattingUtils; +import bisq.network.p2p.storage.P2PDataStorage; + import bisq.common.crypto.KeyRing; import bisq.common.util.MathUtils; @@ -72,12 +74,12 @@ private MarketAlerts(OfferBookService offerBookService, MobileNotificationServic public void onAllServicesInitialized() { offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override - public void onAdded(Offer offer) { + public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { onOfferAdded(offer); } @Override - public void onRemoved(Offer offer) { + public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { } }); applyFilterOnAllOffers(); diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 64b39004634..61a7d781b7b 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -24,6 +24,7 @@ import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.p2p.storage.payload.ProtectedStorageEntry; import bisq.common.UserThread; @@ -44,22 +45,23 @@ import java.util.Objects; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static bisq.network.p2p.storage.P2PDataStorage.get32ByteHashAsByteArray; + /** * Handles storage and retrieval of offers. * Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer). */ +@Slf4j public class OfferBookService { - private static final Logger log = LoggerFactory.getLogger(OfferBookService.class); public interface OfferBookChangedListener { - void onAdded(Offer offer); + void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber); - void onRemoved(Offer offer); + void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber); } private final P2PService p2PService; @@ -92,7 +94,8 @@ public void onAdded(Collection protectedStorageEntries) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - listener.onAdded(offer); + P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStorageEntry); + listener.onAdded(offer, hashOfPayload, protectedStorageEntry.getSequenceNumber()); } })); } @@ -104,7 +107,8 @@ public void onRemoved(Collection protectedStorageEntries) OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - listener.onRemoved(offer); + P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStorageEntry); + listener.onRemoved(offer, hashOfPayload, protectedStorageEntry.getSequenceNumber()); } })); } @@ -116,12 +120,12 @@ public void onRemoved(Collection protectedStorageEntries) public void onUpdatedDataReceived() { addOfferBookChangedListener(new OfferBookChangedListener() { @Override - public void onAdded(Offer offer) { + public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { doDumpStatistics(); } @Override - public void onRemoved(Offer offer) { + public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { doDumpStatistics(); } }); 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..48419ea6edb 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 @@ -21,8 +21,8 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; import bisq.core.offer.OfferRestrictions; -import bisq.core.trade.TradeManager; +import bisq.network.p2p.storage.P2PDataStorage; import bisq.network.utils.Utils; import javax.inject.Inject; @@ -32,6 +32,7 @@ import javafx.collections.ObservableList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -52,8 +53,8 @@ public class OfferBook { private final OfferBookService offerBookService; private final ObservableList offerBookListItems = FXCollections.observableArrayList(); - private final Map buyOfferCountMap = new HashMap<>(); - private final Map sellOfferCountMap = new HashMap<>(); + private final Map buyOfferCountMap = new HashMap<>(); // TODO what is this for? + private final Map sellOfferCountMap = new HashMap<>(); // TODO what is this for? private final FilterManager filterManager; /////////////////////////////////////////////////////////////////////////////////////////// @@ -61,13 +62,14 @@ public class OfferBook { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - OfferBook(OfferBookService offerBookService, TradeManager tradeManager, FilterManager filterManager) { + OfferBook(OfferBookService offerBookService, FilterManager filterManager) { this.offerBookService = offerBookService; this.filterManager = filterManager; offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override - public void onAdded(Offer offer) { + public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + printOfferBookListItems("Before onAdded"); // We get onAdded called every time a new ProtectedStorageEntry is received. // Mostly it is the same OfferPayload but the ProtectedStorageEntry is different. // We filter here to only add new offers if the same offer (using equals) was not already added and it @@ -83,44 +85,138 @@ public void onAdded(Offer offer) { return; } - boolean hasSameOffer = offerBookListItems.stream() - .anyMatch(item -> item.getOffer().equals(offer)); + // Use offer.equals(offer) to see if the OfferBook list contains an exact + // match -- offer.equals(offer) includes comparisons of payload, state + // and errorMessage. + boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); if (!hasSameOffer) { - OfferBookListItem offerBookListItem = new OfferBookListItem(offer); - // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. - // If we have an offer with same ID we remove it and add the new offer as it might have a changed state. - Optional candidateWithSameId = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) - .findAny(); - if (candidateWithSameId.isPresent()) { - log.warn("We had an old offer in the list with the same Offer ID. We remove the old one. " + - "old offerBookListItem={}, new offerBookListItem={}", candidateWithSameId.get(), offerBookListItem); - offerBookListItems.remove(candidateWithSameId.get()); + OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer, hashOfPayload, sequenceNumber); + removeAnyOldOfferBookListItemsBeforeAddingReplacement(newOfferBookListItem); + offerBookListItems.add(newOfferBookListItem); // Add replacement. + if (log.isDebugEnabled()) { + log.debug("onAdded: Added new offer {}\n" + + "\t\tonAdded.seqNo {} newItem.seqNo: {} newItem.payloadHash: {}", + offer.getId(), + sequenceNumber, + newOfferBookListItem.getSequenceNumber(), + newOfferBookListItem.hashOfPayload == null ? "null" : newOfferBookListItem.hashOfPayload.getHex()); } - - offerBookListItems.add(offerBookListItem); } else { log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); } + printOfferBookListItems("After onAdded"); } @Override - public void onRemoved(Offer offer) { - removeOffer(offer, tradeManager); + public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + printOfferBookListItems("Before onRemoved"); + removeOffer(offer, hashOfPayload, sequenceNumber); + printOfferBookListItems("After onRemoved"); } }); } - public void removeOffer(Offer offer, TradeManager tradeManager) { + private void removeAnyOldOfferBookListItemsBeforeAddingReplacement(OfferBookListItem newOfferBookListItem) { + String offerId = newOfferBookListItem.getOffer().getId(); + List offerItemsWithSameIdAndDifferentHash = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offerId) && ( + item.hashOfPayload == null + || !item.hashOfPayload.equals(newOfferBookListItem.hashOfPayload)) + ) + .collect(Collectors.toList()); + if (offerItemsWithSameIdAndDifferentHash.size() > 0) { + offerItemsWithSameIdAndDifferentHash.forEach(oldOfferItem -> { + offerBookListItems.remove(oldOfferItem); + if (log.isDebugEnabled()) { + log.debug("onAdded: Removed old offer {} from list\n" + + "\told.seqNo = {} old.payloadHash = {}\n" + + "\tThis may make a subsequent onRemoved( {} ) call redundant.", + offerId, + oldOfferItem.getSequenceNumber(), + oldOfferItem.getHashOfPayload() == null ? "null" : oldOfferItem.getHashOfPayload().getHex(), + oldOfferItem.getOffer().getId()); + } + }); + } + } + + public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { // Update state in case that that offer is used in the take offer screen, so it gets updated correctly offer.setState(Offer.State.REMOVED); - offer.cancelAvailabilityRequest(); - // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. + + if (log.isDebugEnabled()) { + log.debug("onRemoved: id={} \n\tpayload-hash={} \n\tseq-no={}", + offer.getId(), + hashOfPayload.getHex(), + sequenceNumber); + } + + // Find the removal candidate in the OfferBook list with matching offer-id and payload-hash. Optional candidateToRemove = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) + .filter(item -> item.getOffer().getId().equals(offer.getId()) && ( + item.hashOfPayload == null + || item.hashOfPayload.equals(hashOfPayload)) + ) .findAny(); - candidateToRemove.ifPresent(offerBookListItems::remove); + + if (!candidateToRemove.isPresent()) { + // Candidate is not in list, print reason and return. + if (log.isDebugEnabled()) { + log.debug("List does not contain offer with id {} and payload-hash {}", + offer.getId(), + hashOfPayload.getHex()); + } + return; + } + + OfferBookListItem candidate = candidateToRemove.get(); + + // Remove the candidate only if the storage sequenceNumber has increased, and the candidate's + // storage payload hash matches the onRemoved hashOfPayload parameter. We may receive add/remove + // messages out of order (from api 'editoffer'), so we use the sequenceNumber and payload hash to + // ensure we do not remove an edited offer immediately after it was added. + + if (candidate.getSequenceNumber() >= sequenceNumber) { + // Candidate's seq-no is not < onRemoved.seq-no, print reason for not removing candidate and return. + if (log.isDebugEnabled()) { + log.debug("Candidate.seqNo: {} < onRemoved.seqNo: {} ?" + + " No, old offer not removed", + candidate.getSequenceNumber(), + sequenceNumber); + } + return; + } + + // The seq-no test for removal has passed; ensure the candidate is removed + // only if its payload hash matches the method arg (payload hash.). + if (log.isDebugEnabled()) { + // Print the seq-no test has passed. + log.debug("Candidate.seqNo: {} < onRemoved.seqNo: {} ? Yes", + candidate.getSequenceNumber(), + sequenceNumber); + } + + if ((candidate.getHashOfPayload() == null || candidate.getHashOfPayload().equals(hashOfPayload))) { + // The payload-hash test passed, remove the candidate and print reason. + offerBookListItems.remove(candidate); + + if (log.isDebugEnabled()) { + log.debug("Candidate.payload-hash: {} is null or == onRemoved.payload-hash: {} ?" + + " Yes, removed old offer", + candidate.hashOfPayload == null ? "null" : candidate.hashOfPayload.getHex(), + hashOfPayload == null ? "null" : hashOfPayload.getHex()); + } + } else { + if (log.isDebugEnabled()) { + // Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash. + // Print reason for not removing candidate. + log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + + " No, old offer not removed", + candidate.hashOfPayload == null ? "null" : candidate.hashOfPayload.getHex(), + hashOfPayload == null ? "null" : hashOfPayload.getHex()); + } + } } public ObservableList getOfferBookListItems() { @@ -141,8 +237,21 @@ public void fillOfferBookListItems() { log.debug("offerBookListItems.size {}", offerBookListItems.size()); fillOfferCountMaps(); } catch (Throwable t) { - t.printStackTrace(); - log.error("Error at fillOfferBookListItems: " + t.toString()); + log.error("Error at fillOfferBookListItems: " + t); + } + } + + public void printOfferBookListItems(String msg) { + if (log.isDebugEnabled()) { + if (offerBookListItems.size() == 0) { + log.debug("{} -> OfferBookListItems: none", msg); + return; + } + + StringBuilder stringBuilder = new StringBuilder(msg + " -> ").append("OfferBookListItems:").append("\n"); + offerBookListItems.forEach(i -> stringBuilder.append("\t").append(i.toString()).append("\n")); + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + log.debug(stringBuilder.toString()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java index 4d389239935..80a986e16b5 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java @@ -27,6 +27,8 @@ import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentMethod; +import bisq.network.p2p.storage.P2PDataStorage; + import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; @@ -38,18 +40,47 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; -@Slf4j +import javax.annotation.Nullable; +@Slf4j public class OfferBookListItem { @Getter private final Offer offer; + // The protected storage payload hash helps prevent edited offers from being + // mistakenly removed from a UI user's OfferBook list when onRemoved(offer) is called + // after onAdded(offer). (Checking the offer-id is not enough.) This msg order + // problem does not happen when the UI edits an offer because the remove/add msgs are + // always sent in separate envelope bundles, but it can happen when the API is used to + // edit an offer because the remove/add msgs are sent in the same envelope bundle. + // A null value indicates the item's payload hash has not been set by onAdded or + // onRemoved since the most recent OfferBook view refresh. + @Nullable + @Getter + P2PDataStorage.ByteArray hashOfPayload; + + // The sequence number should also be checked with the hashOfPayload, to + // prevent offers from being mistakenly removed from a UI user's OfferBook list + // when onRemoved(offer) is called immediately after onAdded(offer). + // A -1 value indicates the seq-no has not been set by onAdded or onRemoved + // since the most recent OfferBook view refresh. + @Getter + private int sequenceNumber; + // We cache the data once created for performance reasons. AccountAgeWitnessService calls can // be a bit expensive. private WitnessAgeData witnessAgeData; public OfferBookListItem(Offer offer) { + this(offer, null, -1); + } + + public OfferBookListItem(Offer offer, + P2PDataStorage.ByteArray hashOfPayload, + int sequenceNumber) { this.offer = offer; + this.hashOfPayload = hashOfPayload; + this.sequenceNumber = sequenceNumber; } public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, @@ -79,7 +110,7 @@ public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitne // either signed & limits lifted, or waiting for limits to be lifted // Or banned daysSinceSignedAsLong = TimeUnit.MILLISECONDS.toDays(optionalWitness.map(witness -> - accountAgeWitnessService.getWitnessSignAge(witness, new Date())) + accountAgeWitnessService.getWitnessSignAge(witness, new Date())) .orElse(0L)); displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", daysSinceSignedAsLong); info = Res.get("offerbook.timeSinceSigning.info", signState.getDisplayString()); @@ -107,6 +138,16 @@ public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitne return witnessAgeData; } + @Override + public String toString() { + return "OfferBookListItem{" + + "offerId=" + offer.getId() + + ", hashOfPayload=" + (hashOfPayload == null ? "null" : hashOfPayload.getHex()) + + ", sequenceNumber=" + sequenceNumber + + ", witnessAgeData=" + (witnessAgeData == null ? "null" : witnessAgeData.displayString) + + '}'; + } + @Value public static class WitnessAgeData { private final String displayString; diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 1e01c8a80ab..c82b043efb4 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -298,7 +298,7 @@ public void onClose(boolean removeOffer) { // only local effect. Other trader might see the offer for a few seconds // still (but cannot take it). if (removeOffer) { - offerBook.removeOffer(checkNotNull(offer), tradeManager); + offerBook.removeOffer(checkNotNull(offer), null, -1); } btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); diff --git a/desktop/src/main/resources/logback.xml b/desktop/src/main/resources/logback.xml index c21adbf3255..78ef0c99901 100644 --- a/desktop/src/main/resources/logback.xml +++ b/desktop/src/main/resources/logback.xml @@ -1,13 +1,16 @@ - + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + - From d3508d2037ccc8c2b1279e1d0975a95341b46b1d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 7 Aug 2021 14:11:47 -0300 Subject: [PATCH 44/56] Revert logback debug config changes (Accidentally included in last commit.) --- desktop/src/main/resources/logback.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/desktop/src/main/resources/logback.xml b/desktop/src/main/resources/logback.xml index 78ef0c99901..52dd4e1e9b8 100644 --- a/desktop/src/main/resources/logback.xml +++ b/desktop/src/main/resources/logback.xml @@ -1,15 +1,11 @@ - + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) - - - - From 713867b990a3419365becbbb87aa6066c67df92b Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 11 Aug 2021 11:29:17 -0300 Subject: [PATCH 45/56] Remove comment (question answered) --- .../java/bisq/desktop/main/offer/offerbook/OfferBook.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 48419ea6edb..3da3ffad35d 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 @@ -53,8 +53,8 @@ public class OfferBook { private final OfferBookService offerBookService; private final ObservableList offerBookListItems = FXCollections.observableArrayList(); - private final Map buyOfferCountMap = new HashMap<>(); // TODO what is this for? - private final Map sellOfferCountMap = new HashMap<>(); // TODO what is this for? + private final Map buyOfferCountMap = new HashMap<>(); + private final Map sellOfferCountMap = new HashMap<>(); private final FilterManager filterManager; /////////////////////////////////////////////////////////////////////////////////////////// From 6a4aceda7bd90772d882af3e3adfef3544a22ad1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 13 Aug 2021 14:53:17 -0300 Subject: [PATCH 46/56] Handle API's edit+disable offer use case in UI. This change is a refactoring for handling the removal of a peer UI's offer item when it is deactivated and edited in the same CLI `editoffer` command. On the API side, an `editoffer --price=N --enable=false` command results in the edited offer not being re-published. On a UI peer's side, the edited offer is not added to the peer's storage, and the peer's onRemoved(offer) listener event does not find a storage entry with matching payload-hash. This fix assumes an offer that is not in the local store should be removed from the UI's view list -- when the onRemoved method's hashOfPayload does not match the UI's view list item's hashOfPayload. --- .../main/offer/offerbook/OfferBook.java | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) 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 3da3ffad35d..556f594d1f4 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 @@ -148,35 +148,52 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int if (log.isDebugEnabled()) { log.debug("onRemoved: id={} \n\tpayload-hash={} \n\tseq-no={}", offer.getId(), - hashOfPayload.getHex(), + hashOfPayload == null ? "null" : hashOfPayload.getHex(), sequenceNumber); } - // Find the removal candidate in the OfferBook list with matching offer-id and payload-hash. - Optional candidateToRemove = offerBookListItems.stream() + // Find the removal candidate in the OfferBook list with matching offerId and payload-hash. + Optional candidateWithMatchingPayloadHash = offerBookListItems.stream() .filter(item -> item.getOffer().getId().equals(offer.getId()) && ( item.hashOfPayload == null || item.hashOfPayload.equals(hashOfPayload)) ) .findAny(); - if (!candidateToRemove.isPresent()) { - // Candidate is not in list, print reason and return. + if (!candidateWithMatchingPayloadHash.isPresent()) { if (log.isDebugEnabled()) { - log.debug("List does not contain offer with id {} and payload-hash {}", + log.debug("UI view list does not contain offer with id {} and payload-hash {}", offer.getId(), - hashOfPayload.getHex()); + hashOfPayload == null ? "null" : hashOfPayload.getHex()); + } + + // The OfferBookListItem with a null or matching payload-hash was not found. + // However, when the API's CLI is used to edit and deactivate an offer + // in the same command, the edited offer is not re-published (and cannot be + // found in local storage). In this case, we need to remove the deactivated + // offer from the list if the local store does not contain an offer with a + // matching offerId. + if (!isStoredLocally(offer)) { + Optional viewItem = getOfferBookListItem(offer); + viewItem.ifPresent((item) -> { + offerBookListItems.remove(item); + if (log.isDebugEnabled()) { + log.debug("Storage does not contain an offer with id {} either;" + + " it is removed from UI view list.", + offer.getId()); + } + }); } + return; } - OfferBookListItem candidate = candidateToRemove.get(); + OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); // Remove the candidate only if the storage sequenceNumber has increased, and the candidate's // storage payload hash matches the onRemoved hashOfPayload parameter. We may receive add/remove // messages out of order (from api 'editoffer'), so we use the sequenceNumber and payload hash to // ensure we do not remove an edited offer immediately after it was added. - if (candidate.getSequenceNumber() >= sequenceNumber) { // Candidate's seq-no is not < onRemoved.seq-no, print reason for not removing candidate and return. if (log.isDebugEnabled()) { @@ -185,18 +202,19 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int candidate.getSequenceNumber(), sequenceNumber); } + return; } - // The seq-no test for removal has passed; ensure the candidate is removed - // only if its payload hash matches the method arg (payload hash.). + // The seq-no test for removal has passed. if (log.isDebugEnabled()) { // Print the seq-no test has passed. log.debug("Candidate.seqNo: {} < onRemoved.seqNo: {} ? Yes", candidate.getSequenceNumber(), sequenceNumber); } - + // Ensure the candidate is removed only if its payload hash matches the + // method's hashOfPayload param. if ((candidate.getHashOfPayload() == null || candidate.getHashOfPayload().equals(hashOfPayload))) { // The payload-hash test passed, remove the candidate and print reason. offerBookListItems.remove(candidate); @@ -229,8 +247,8 @@ public void fillOfferBookListItems() { // Investigate why.... offerBookListItems.clear(); offerBookListItems.addAll(offerBookService.getOffers().stream() - .filter(o -> !filterManager.isOfferIdBanned(o.getId())) - .filter(o -> !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(o.getMakerNodeAddress().getHostName())) + .filter(o -> !isOfferIdBanned(o)) + .filter(o -> isV3NodeAddressCompliant(o)) .map(OfferBookListItem::new) .collect(Collectors.toList())); @@ -263,6 +281,30 @@ public Map getSellOfferCountMap() { return sellOfferCountMap; } + + private Optional getOfferBookListItem(Offer offer) { + return offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offer.getId())) + .findFirst(); + } + + private boolean isOfferIdBanned(Offer offer) { + return filterManager.isOfferIdBanned(offer.getId()); + } + + private boolean isV3NodeAddressCompliant(Offer offer) { + return !OfferRestrictions.requiresNodeAddressUpdate() + || Utils.isV3Address(offer.getMakerNodeAddress().getHostName()); + } + + private boolean isStoredLocally(Offer offer) { + return offerBookService.getOffers().stream() + .anyMatch(o -> o.getId().equals(offer.getId()) + && !isOfferIdBanned(o) + && isV3NodeAddressCompliant(o) + ); + } + private void fillOfferCountMaps() { buyOfferCountMap.clear(); sellOfferCountMap.clear(); From 71a61c63dacb2aec7bddaabb93d1643d837981c6 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sat, 14 Aug 2021 16:58:47 -0300 Subject: [PATCH 47/56] Display Enabled=PENDING in CLI 'createoffer' output A newly created offer has no OpenOffer+State (AVAILABLE || DEACTIVATED) when displayed in the CLI's console. This change adds a 'bool isMyPendingOffer' to the OfferInfo proto + wrapper, and the CLI's console offer output formatter uses it to determine if it should display a new offer's Enabled column value as PENDING, instead of an ambiguous NO value. --- .../method/offer/CreateBSQOffersTest.java | 21 ++++++++++++++ .../offer/CreateOfferUsingFixedPriceTest.java | 16 +++++++++++ ...CreateOfferUsingMarketPriceMarginTest.java | 26 +++++++++++++++++ cli/src/main/java/bisq/cli/CliMain.java | 2 +- cli/src/main/java/bisq/cli/TableFormat.java | 12 ++++++-- .../java/bisq/core/api/model/OfferInfo.java | 28 ++++++++++++++++--- .../bisq/daemon/grpc/GrpcOffersService.java | 4 +-- proto/src/main/proto/grpc.proto | 1 + 8 files changed, 101 insertions(+), 9 deletions(-) diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java index ba4f8ce47b6..652d7f50dcf 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateBSQOffersTest.java @@ -39,6 +39,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; @@ -70,6 +71,9 @@ public void testCreateBuy1BTCFor20KBSQOffer() { alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); log.info("Sell BSQ (Buy BTC) OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); @@ -86,6 +90,8 @@ public void testCreateBuy1BTCFor20KBSQOffer() { genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); @@ -112,6 +118,9 @@ public void testCreateSell1BTCFor20KBSQOffer() { alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); log.info("SELL 20K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); @@ -128,6 +137,8 @@ public void testCreateSell1BTCFor20KBSQOffer() { genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); @@ -154,6 +165,9 @@ public void testCreateBuyBTCWith1To2KBSQOffer() { alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); log.info("BUY 1-2K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); @@ -170,6 +184,8 @@ public void testCreateBuyBTCWith1To2KBSQOffer() { genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); @@ -196,6 +212,9 @@ public void testCreateSellBTCFor5To10KBSQOffer() { alicesBsqAcct.getId(), MAKER_FEE_CURRENCY_CODE); log.info("SELL 5-10K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); @@ -212,6 +231,8 @@ public void testCreateSellBTCFor5To10KBSQOffer() { genBtcBlockAndWaitForOfferPreparation(); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java index 081c6feadc7..715e05a92e7 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingFixedPriceTest.java @@ -35,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.SELL; @@ -58,6 +59,9 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { audAccount.getId(), MAKER_FEE_CURRENCY_CODE); log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "AUD")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); @@ -72,6 +76,8 @@ public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() { assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); @@ -98,6 +104,9 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { usdAccount.getId(), MAKER_FEE_CURRENCY_CODE); log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "USD")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); @@ -112,6 +121,8 @@ public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() { assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); @@ -138,6 +149,9 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { eurAccount.getId(), MAKER_FEE_CURRENCY_CODE); log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "EUR")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); @@ -152,6 +166,8 @@ public void testCreateEURBTCSellOfferUsingFixedPrice95001234() { assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertFalse(newOffer.getUseMarketBasedPrice()); 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 451bd1f2c18..391bb4c5a37 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -49,6 +49,7 @@ 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static protobuf.OfferPayload.Direction.BUY; @@ -81,6 +82,9 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); @@ -94,6 +98,8 @@ public void testCreateUSDBTCBuyOffer5PctPriceMargin() { assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); @@ -123,6 +129,9 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(BUY.name(), newOffer.getDirection()); @@ -136,6 +145,8 @@ public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() { assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(BUY.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); @@ -165,6 +176,9 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); @@ -178,6 +192,8 @@ public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() { assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); @@ -207,6 +223,9 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { MAKER_FEE_CURRENCY_CODE, NO_TRIGGER_PRICE); log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl")); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + String newOfferId = newOffer.getId(); assertNotEquals("", newOfferId); assertEquals(SELL.name(), newOffer.getDirection()); @@ -220,6 +239,8 @@ public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() { assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); newOffer = aliceClient.getMyOffer(newOfferId); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(newOfferId, newOffer.getId()); assertEquals(SELL.name(), newOffer.getDirection()); assertTrue(newOffer.getUseMarketBasedPrice()); @@ -252,9 +273,14 @@ public void testCreateUSDBTCBuyOfferWithTriggerPrice() { usdAccount.getId(), MAKER_FEE_CURRENCY_CODE, triggerPriceAsLong); + assertTrue(newOffer.getIsMyOffer()); + assertTrue(newOffer.getIsMyPendingOffer()); + genBtcBlocksThenWait(1, 4000); // give time to add to offer book newOffer = aliceClient.getMyOffer(newOffer.getId()); log.info("OFFER #5:\n{}", formatOfferTable(singletonList(newOffer), "usd")); + assertTrue(newOffer.getIsMyOffer()); + assertFalse(newOffer.getIsMyPendingOffer()); assertEquals(triggerPriceAsLong, newOffer.getTriggerPrice()); } diff --git a/cli/src/main/java/bisq/cli/CliMain.java b/cli/src/main/java/bisq/cli/CliMain.java index bf3978675c6..bd57db2e9c9 100644 --- a/cli/src/main/java/bisq/cli/CliMain.java +++ b/cli/src/main/java/bisq/cli/CliMain.java @@ -509,7 +509,7 @@ public static void run(String[] args) { } var tradeId = opts.getTradeId(); var address = opts.getAddress(); - // Multi-word memos must be double quoted. + // Multi-word memos must be double-quoted. var memo = opts.getMemo(); client.withdrawFunds(tradeId, address, memo); out.printf("trade %s funds sent to btc address %s%n", tradeId, address); diff --git a/cli/src/main/java/bisq/cli/TableFormat.java b/cli/src/main/java/bisq/cli/TableFormat.java index 3d14b5991d4..1340ac3a760 100644 --- a/cli/src/main/java/bisq/cli/TableFormat.java +++ b/cli/src/main/java/bisq/cli/TableFormat.java @@ -202,7 +202,7 @@ private static String formattedFiatOfferTable(List offers, return headerLine + offers.stream() .map(o -> format(colDataFormat, - o.getIsActivated() ? "YES" : "NO", + formatEnabled(o), o.getDirection(), formatPrice(o.getPrice()), formatAmountRange(o.getMinAmount(), o.getAmount()), @@ -300,7 +300,7 @@ private static String formatCryptoCurrencyOfferTable(List offers, return headerLine + offers.stream() .map(o -> format(colDataFormat, - o.getIsActivated() ? "YES" : "NO", + formatEnabled(o), directionFormat.apply(o), formatCryptoCurrencyPrice(o.getPrice()), formatAmountRange(o.getMinAmount(), o.getAmount()), @@ -324,6 +324,14 @@ private static String formatCryptoCurrencyOfferTable(List offers, } } + + private static String formatEnabled(OfferInfo offerInfo) { + if (offerInfo.getIsMyOffer() && offerInfo.getIsMyPendingOffer()) + return "PENDING"; + else + return offerInfo.getIsActivated() ? "YES" : "NO"; + } + private static int getLongestPaymentMethodColWidth(List offers) { return getLongestColumnSize( COL_HEADER_PAYMENT_METHOD.length(), 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 f99df264569..15ad2acc108 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,8 @@ public class OfferInfo implements Payload { private final long date; private final String state; private final boolean isActivated; - private boolean isMyOffer; + private boolean isMyOffer; // Not final -- may be re-set after instantiation. + private final boolean isMyPendingOffer; public OfferInfo(OfferInfoBuilder builder) { this.id = builder.id; @@ -91,11 +92,12 @@ public OfferInfo(OfferInfoBuilder builder) { this.state = builder.state; this.isActivated = builder.isActivated; this.isMyOffer = builder.isMyOffer; + this.isMyPendingOffer = builder.isMyPendingOffer; } - // Allow isMyOffer to be set on new offers' OfferInfo instances. - public void setIsMyOffer(boolean myOffer) { - isMyOffer = myOffer; + // Allow isMyOffer to be set on a new offer's OfferInfo instance. + public void setIsMyOffer(boolean isMyOffer) { + this.isMyOffer = isMyOffer; } public static OfferInfo toOfferInfo(Offer offer) { @@ -104,6 +106,16 @@ public static OfferInfo toOfferInfo(Offer offer) { return getOfferInfoBuilder(offer, false).build(); } + public static OfferInfo toPendingOfferInfo(Offer myNewOffer) { + // Use this to build an OfferInfo instance when a new OpenOffer is being + // prepared, and no valid OpenOffer state (AVAILABLE, DEACTIVATED) exists. + // It is needed for the CLI's 'createoffer' output, which has a boolean 'ENABLED' + // column that will show a PENDING value when this.isMyPendingOffer = true. + return getOfferInfoBuilder(myNewOffer, true) + .withIsMyPendingOffer(true) + .build(); + } + public static OfferInfo toOfferInfo(OpenOffer openOffer) { // An OpenOffer is always my offer. return getOfferInfoBuilder(openOffer.getOffer(), true) @@ -171,6 +183,7 @@ public bisq.proto.grpc.OfferInfo toProtoMessage() { .setState(state) .setIsActivated(isActivated) .setIsMyOffer(isMyOffer) + .setIsMyPendingOffer(isMyPendingOffer) .build(); } @@ -202,6 +215,7 @@ public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) { .withState(proto.getState()) .withIsActivated(proto.getIsActivated()) .withIsMyOffer(proto.getIsMyOffer()) + .withIsMyPendingOffer(proto.getIsMyPendingOffer()) .build(); } @@ -237,6 +251,7 @@ public static class OfferInfoBuilder { private String state; private boolean isActivated; private boolean isMyOffer; + private boolean isMyPendingOffer; public OfferInfoBuilder withId(String id) { this.id = id; @@ -363,6 +378,11 @@ public OfferInfoBuilder withIsMyOffer(boolean isMyOffer) { return this; } + public OfferInfoBuilder withIsMyPendingOffer(boolean isMyPendingOffer) { + this.isMyPendingOffer = isMyPendingOffer; + 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 b1f9212d46b..e31828bbfb8 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcOffersService.java @@ -50,6 +50,7 @@ import lombok.extern.slf4j.Slf4j; import static bisq.core.api.model.OfferInfo.toOfferInfo; +import static bisq.core.api.model.OfferInfo.toPendingOfferInfo; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static bisq.proto.grpc.OffersGrpc.*; import static java.util.concurrent.TimeUnit.MINUTES; @@ -160,8 +161,7 @@ public void createOffer(CreateOfferRequest req, offer -> { // 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); + OfferInfo offerInfo = toPendingOfferInfo(offer); CreateOfferReply reply = CreateOfferReply.newBuilder() .setOffer(offerInfo.toProtoMessage()) .build(); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index ff2467f713f..21221eda452 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -192,6 +192,7 @@ message OfferInfo { uint64 makerFee = 23; bool isActivated = 24; bool isMyOffer = 25; + bool isMyPendingOffer = 26; } message AvailabilityResultWithDescription { From d709338d6366294ef727b0ab561e7ba73384872a Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 16 Aug 2021 14:17:28 -0300 Subject: [PATCH 48/56] Refactor 2 predicates as single predicate Resolves https://github.com/bisq-network/bisq/pull/5659#discussion_r689631147 --- .../main/offer/offerbook/OfferBook.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) 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 556f594d1f4..7eb9a1777fa 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 @@ -247,8 +247,7 @@ public void fillOfferBookListItems() { // Investigate why.... offerBookListItems.clear(); offerBookListItems.addAll(offerBookService.getOffers().stream() - .filter(o -> !isOfferIdBanned(o)) - .filter(o -> isV3NodeAddressCompliant(o)) + .filter(o -> isOfferAllowed(o)) .map(OfferBookListItem::new) .collect(Collectors.toList())); @@ -288,21 +287,16 @@ private Optional getOfferBookListItem(Offer offer) { .findFirst(); } - private boolean isOfferIdBanned(Offer offer) { - return filterManager.isOfferIdBanned(offer.getId()); - } - - private boolean isV3NodeAddressCompliant(Offer offer) { - return !OfferRestrictions.requiresNodeAddressUpdate() + private boolean isOfferAllowed(Offer offer) { + boolean isBanned = filterManager.isOfferIdBanned(offer.getId()); + boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offer.getMakerNodeAddress().getHostName()); + return !isBanned && isV3NodeAddressCompliant; } private boolean isStoredLocally(Offer offer) { return offerBookService.getOffers().stream() - .anyMatch(o -> o.getId().equals(offer.getId()) - && !isOfferIdBanned(o) - && isV3NodeAddressCompliant(o) - ); + .anyMatch(o -> o.getId().equals(offer.getId()) && isOfferAllowed(o)); } private void fillOfferCountMaps() { From 59c031327725bfc669414389056b875eff9a9faa Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Mon, 16 Aug 2021 14:23:28 -0300 Subject: [PATCH 49/56] Fix long method name Resolves https://github.com/bisq-network/bisq/pull/5659#discussion_r687839333 --- .../java/bisq/desktop/main/offer/offerbook/OfferBook.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7eb9a1777fa..b6904639a6f 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 @@ -91,7 +91,7 @@ public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int seq boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); if (!hasSameOffer) { OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer, hashOfPayload, sequenceNumber); - removeAnyOldOfferBookListItemsBeforeAddingReplacement(newOfferBookListItem); + removeDuplicateItem(newOfferBookListItem); offerBookListItems.add(newOfferBookListItem); // Add replacement. if (log.isDebugEnabled()) { log.debug("onAdded: Added new offer {}\n" @@ -116,7 +116,7 @@ public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int s }); } - private void removeAnyOldOfferBookListItemsBeforeAddingReplacement(OfferBookListItem newOfferBookListItem) { + private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { String offerId = newOfferBookListItem.getOffer().getId(); List offerItemsWithSameIdAndDifferentHash = offerBookListItems.stream() .filter(item -> item.getOffer().getId().equals(offerId) && ( From fb4e00fb6bf6c6fbac536292d3b930f3c04cacf0 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Aug 2021 14:27:20 -0300 Subject: [PATCH 50/56] Fix OfferBookService bug causing extra check in OfferBook.onRemoved Hash of protectedStorageEntry (should be offerPayload) was sometimes resulting in incorrect hash being sent to OfferBook listener methods onAdded(offer, hashOfPayload, sequenceNumber), and onRemoved(offer, hashOfPayload, sequenceNumber). Hash of OfferPayload is correctly passed to listener with this change. Sending the correct hash allows removal of a dubious code block that removed a book view list item when hash compare failed, and no matching offer existed in the OfferBookService. See https://github.com/bisq-network/bisq/pull/5659#discussion_r689634240 --- .../bisq/core/offer/OfferBookService.java | 4 ++-- .../main/offer/offerbook/OfferBook.java | 19 ------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 61a7d781b7b..a589e187452 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -94,7 +94,7 @@ public void onAdded(Collection protectedStorageEntries) { OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStorageEntry); + P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(offerPayload); listener.onAdded(offer, hashOfPayload, protectedStorageEntry.getSequenceNumber()); } })); @@ -107,7 +107,7 @@ public void onRemoved(Collection protectedStorageEntries) OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); - P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStorageEntry); + P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(offerPayload); listener.onRemoved(offer, hashOfPayload, protectedStorageEntry.getSequenceNumber()); } })); 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 b6904639a6f..351550e3930 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 @@ -166,25 +166,6 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int offer.getId(), hashOfPayload == null ? "null" : hashOfPayload.getHex()); } - - // The OfferBookListItem with a null or matching payload-hash was not found. - // However, when the API's CLI is used to edit and deactivate an offer - // in the same command, the edited offer is not re-published (and cannot be - // found in local storage). In this case, we need to remove the deactivated - // offer from the list if the local store does not contain an offer with a - // matching offerId. - if (!isStoredLocally(offer)) { - Optional viewItem = getOfferBookListItem(offer); - viewItem.ifPresent((item) -> { - offerBookListItems.remove(item); - if (log.isDebugEnabled()) { - log.debug("Storage does not contain an offer with id {} either;" - + " it is removed from UI view list.", - offer.getId()); - } - }); - } - return; } From fdc78b2fb5b223e91363f299d1cda98e2b084c12 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 18 Aug 2021 14:48:55 -0300 Subject: [PATCH 51/56] Remove unused methods --- .../bisq/desktop/main/offer/offerbook/OfferBook.java | 12 ------------ 1 file changed, 12 deletions(-) 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 351550e3930..374261423e1 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 @@ -261,13 +261,6 @@ public Map getSellOfferCountMap() { return sellOfferCountMap; } - - private Optional getOfferBookListItem(Offer offer) { - return offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) - .findFirst(); - } - private boolean isOfferAllowed(Offer offer) { boolean isBanned = filterManager.isOfferIdBanned(offer.getId()); boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() @@ -275,11 +268,6 @@ private boolean isOfferAllowed(Offer offer) { return !isBanned && isV3NodeAddressCompliant; } - private boolean isStoredLocally(Offer offer) { - return offerBookService.getOffers().stream() - .anyMatch(o -> o.getId().equals(offer.getId()) && isOfferAllowed(o)); - } - private void fillOfferCountMaps() { buyOfferCountMap.clear(); sellOfferCountMap.clear(); From b93f6ea28ebf4001c46762d3b59ca6b45dc3e5ad Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Fri, 20 Aug 2021 11:49:00 -0300 Subject: [PATCH 52/56] Remove sequence-number checks from OfferBook Checking offer payload hashes in OfferBook's onAdded and onRemove methods is sufficient to prevent incorrect removal of offer list items from the UI OfferBook view (where api 'editoffer' causes onRemoved to be called after onAdded on peers). --- .../alerts/market/MarketAlerts.java | 4 +- .../bisq/core/offer/OfferBookService.java | 12 ++--- .../main/offer/offerbook/OfferBook.java | 52 +++++-------------- .../offer/offerbook/OfferBookListItem.java | 37 ++++++------- .../offer/takeoffer/TakeOfferDataModel.java | 2 +- 5 files changed, 38 insertions(+), 69 deletions(-) diff --git a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java index b4df5082114..936ecd8cf5c 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java +++ b/core/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java @@ -74,12 +74,12 @@ private MarketAlerts(OfferBookService offerBookService, MobileNotificationServic public void onAllServicesInitialized() { offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override - public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { onOfferAdded(offer); } @Override - public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { } }); applyFilterOnAllOffers(); diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index a589e187452..98956bda51b 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -59,9 +59,9 @@ public class OfferBookService { public interface OfferBookChangedListener { - void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber); + void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload); - void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber); + void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload); } private final P2PService p2PService; @@ -95,7 +95,7 @@ public void onAdded(Collection protectedStorageEntries) { Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(offerPayload); - listener.onAdded(offer, hashOfPayload, protectedStorageEntry.getSequenceNumber()); + listener.onAdded(offer, hashOfPayload); } })); } @@ -108,7 +108,7 @@ public void onRemoved(Collection protectedStorageEntries) Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(offerPayload); - listener.onRemoved(offer, hashOfPayload, protectedStorageEntry.getSequenceNumber()); + listener.onRemoved(offer, hashOfPayload); } })); } @@ -120,12 +120,12 @@ public void onRemoved(Collection protectedStorageEntries) public void onUpdatedDataReceived() { addOfferBookChangedListener(new OfferBookChangedListener() { @Override - public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { doDumpStatistics(); } @Override - public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { doDumpStatistics(); } }); 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 374261423e1..2e157f75b01 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 @@ -68,7 +68,7 @@ public class OfferBook { offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { @Override - public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { printOfferBookListItems("Before onAdded"); // We get onAdded called every time a new ProtectedStorageEntry is received. // Mostly it is the same OfferPayload but the ProtectedStorageEntry is different. @@ -90,15 +90,13 @@ public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int seq // and errorMessage. boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); if (!hasSameOffer) { - OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer, hashOfPayload, sequenceNumber); + OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer, hashOfPayload); removeDuplicateItem(newOfferBookListItem); offerBookListItems.add(newOfferBookListItem); // Add replacement. if (log.isDebugEnabled()) { log.debug("onAdded: Added new offer {}\n" - + "\t\tonAdded.seqNo {} newItem.seqNo: {} newItem.payloadHash: {}", + + "\twith newItem.payloadHash: {}", offer.getId(), - sequenceNumber, - newOfferBookListItem.getSequenceNumber(), newOfferBookListItem.hashOfPayload == null ? "null" : newOfferBookListItem.hashOfPayload.getHex()); } } else { @@ -108,9 +106,9 @@ public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int seq } @Override - public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { printOfferBookListItems("Before onRemoved"); - removeOffer(offer, hashOfPayload, sequenceNumber); + removeOffer(offer, hashOfPayload); printOfferBookListItems("After onRemoved"); } }); @@ -128,11 +126,10 @@ private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { offerItemsWithSameIdAndDifferentHash.forEach(oldOfferItem -> { offerBookListItems.remove(oldOfferItem); if (log.isDebugEnabled()) { - log.debug("onAdded: Removed old offer {} from list\n" - + "\told.seqNo = {} old.payloadHash = {}\n" + log.debug("onAdded: Removed old offer {}\n" + + "\twith old payloadHash = {} from list.\n" + "\tThis may make a subsequent onRemoved( {} ) call redundant.", offerId, - oldOfferItem.getSequenceNumber(), oldOfferItem.getHashOfPayload() == null ? "null" : oldOfferItem.getHashOfPayload().getHex(), oldOfferItem.getOffer().getId()); } @@ -140,16 +137,16 @@ private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { } } - public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { + public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { // Update state in case that that offer is used in the take offer screen, so it gets updated correctly offer.setState(Offer.State.REMOVED); offer.cancelAvailabilityRequest(); if (log.isDebugEnabled()) { - log.debug("onRemoved: id={} \n\tpayload-hash={} \n\tseq-no={}", + log.debug("onRemoved: id = {}\n" + + "\twith payload-hash = {}", offer.getId(), - hashOfPayload == null ? "null" : hashOfPayload.getHex(), - sequenceNumber); + hashOfPayload == null ? "null" : hashOfPayload.getHex()); } // Find the removal candidate in the OfferBook list with matching offerId and payload-hash. @@ -171,31 +168,10 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload, int OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); - // Remove the candidate only if the storage sequenceNumber has increased, and the candidate's - // storage payload hash matches the onRemoved hashOfPayload parameter. We may receive add/remove - // messages out of order (from api 'editoffer'), so we use the sequenceNumber and payload hash to + // Remove the candidate only if the candidate's offer payload hash matches the + // onRemoved hashOfPayload parameter. We may receive add/remove messages out of + // order (from api's 'editoffer'), and use the offer payload hash to // ensure we do not remove an edited offer immediately after it was added. - if (candidate.getSequenceNumber() >= sequenceNumber) { - // Candidate's seq-no is not < onRemoved.seq-no, print reason for not removing candidate and return. - if (log.isDebugEnabled()) { - log.debug("Candidate.seqNo: {} < onRemoved.seqNo: {} ?" - + " No, old offer not removed", - candidate.getSequenceNumber(), - sequenceNumber); - } - - return; - } - - // The seq-no test for removal has passed. - if (log.isDebugEnabled()) { - // Print the seq-no test has passed. - log.debug("Candidate.seqNo: {} < onRemoved.seqNo: {} ? Yes", - candidate.getSequenceNumber(), - sequenceNumber); - } - // Ensure the candidate is removed only if its payload hash matches the - // method's hashOfPayload param. if ((candidate.getHashOfPayload() == null || candidate.getHashOfPayload().equals(hashOfPayload))) { // The payload-hash test passed, remove the candidate and print reason. offerBookListItems.remove(candidate); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java index dcb9f05495e..89872c6fa00 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java @@ -49,40 +49,34 @@ public class OfferBookListItem { @Getter private final Offer offer; - // The protected storage payload hash helps prevent edited offers from being - // mistakenly removed from a UI user's OfferBook list when onRemoved(offer) is called - // after onAdded(offer). (Checking the offer-id is not enough.) This msg order - // problem does not happen when the UI edits an offer because the remove/add msgs are - // always sent in separate envelope bundles, but it can happen when the API is used to - // edit an offer because the remove/add msgs are sent in the same envelope bundle. - // A null value indicates the item's payload hash has not been set by onAdded or - // onRemoved since the most recent OfferBook view refresh. + /** + * The protected storage (offer) payload hash helps prevent edited offers from being + * mistakenly removed from a UI user's OfferBook list if the API's 'editoffer' + * command results in onRemoved(offer) being called after onAdded(offer) on peers. + * (Checking the offer-id is not enough.) This msg order problem does not happen + * when the UI edits an offer because the remove/add msgs are always sent in separate + * envelope bundles. It can happen when the API is used to edit an offer because + * the remove/add msgs are received in the same envelope bundle, then processed in + * unpredictable order. + * + * A null value indicates the item's payload hash has not been set by onAdded or + * onRemoved since the most recent OfferBook view refresh. + */ @Nullable @Getter P2PDataStorage.ByteArray hashOfPayload; - // The sequence number should also be checked with the hashOfPayload, to - // prevent offers from being mistakenly removed from a UI user's OfferBook list - // when onRemoved(offer) is called immediately after onAdded(offer). - // A -1 value indicates the seq-no has not been set by onAdded or onRemoved - // since the most recent OfferBook view refresh. - @Getter - private final int sequenceNumber; - // We cache the data once created for performance reasons. AccountAgeWitnessService calls can // be a bit expensive. private WitnessAgeData witnessAgeData; public OfferBookListItem(Offer offer) { - this(offer, null, -1); + this(offer, null); } - public OfferBookListItem(Offer offer, - @Nullable P2PDataStorage.ByteArray hashOfPayload, - int sequenceNumber) { + public OfferBookListItem(Offer offer, @Nullable P2PDataStorage.ByteArray hashOfPayload) { this.offer = offer; this.hashOfPayload = hashOfPayload; - this.sequenceNumber = sequenceNumber; } public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, @@ -131,7 +125,6 @@ public String toString() { return "OfferBookListItem{" + "offerId=" + offer.getId() + ", hashOfPayload=" + (hashOfPayload == null ? "null" : hashOfPayload.getHex()) + - ", sequenceNumber=" + sequenceNumber + ", witnessAgeData=" + (witnessAgeData == null ? "null" : witnessAgeData.displayString) + '}'; } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 79993092f06..00309dc6f8a 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -298,7 +298,7 @@ public void onClose(boolean removeOffer) { // only local effect. Other trader might see the offer for a few seconds // still (but cannot take it). if (removeOffer) { - offerBook.removeOffer(checkNotNull(offer), null, -1); + offerBook.removeOffer(checkNotNull(offer), null); } btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); From 59192e98f1bd490d4db767f2f28263f495cb5475 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Sun, 22 Aug 2021 10:24:18 -0300 Subject: [PATCH 53/56] Do not filter on paylaod hash when deleting duplicate list items Any and all view list items with a matching offerId should be removed from view just before adding a new list item. --- .../desktop/main/offer/offerbook/OfferBook.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 2e157f75b01..3de587ead12 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 @@ -116,18 +116,17 @@ public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { String offerId = newOfferBookListItem.getOffer().getId(); - List offerItemsWithSameIdAndDifferentHash = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offerId) && ( - item.hashOfPayload == null - || !item.hashOfPayload.equals(newOfferBookListItem.hashOfPayload)) - ) + // We need to remove any view items with a matching offerId before + // a newOfferBookListItem is added to the view. + List duplicateItems = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offerId)) .collect(Collectors.toList()); - if (offerItemsWithSameIdAndDifferentHash.size() > 0) { - offerItemsWithSameIdAndDifferentHash.forEach(oldOfferItem -> { + if (duplicateItems.size() > 0) { + duplicateItems.forEach(oldOfferItem -> { offerBookListItems.remove(oldOfferItem); if (log.isDebugEnabled()) { log.debug("onAdded: Removed old offer {}\n" - + "\twith old payloadHash = {} from list.\n" + + "\twith payload hash {} from list.\n" + "\tThis may make a subsequent onRemoved( {} ) call redundant.", offerId, oldOfferItem.getHashOfPayload() == null ? "null" : oldOfferItem.getHashOfPayload().getHex(), From 4889da6bba6ad155436a2dede5182643f706ac6d Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 25 Aug 2021 10:34:22 -0300 Subject: [PATCH 54/56] Remove redundant list.size check --- .../main/offer/offerbook/OfferBook.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) 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 3de587ead12..18ba22b5098 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 @@ -121,19 +121,17 @@ private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { List duplicateItems = offerBookListItems.stream() .filter(item -> item.getOffer().getId().equals(offerId)) .collect(Collectors.toList()); - if (duplicateItems.size() > 0) { - duplicateItems.forEach(oldOfferItem -> { - offerBookListItems.remove(oldOfferItem); - if (log.isDebugEnabled()) { - log.debug("onAdded: Removed old offer {}\n" - + "\twith payload hash {} from list.\n" - + "\tThis may make a subsequent onRemoved( {} ) call redundant.", - offerId, - oldOfferItem.getHashOfPayload() == null ? "null" : oldOfferItem.getHashOfPayload().getHex(), - oldOfferItem.getOffer().getId()); - } - }); - } + duplicateItems.forEach(oldOfferItem -> { + offerBookListItems.remove(oldOfferItem); + if (log.isDebugEnabled()) { + log.debug("onAdded: Removed old offer {}\n" + + "\twith payload hash {} from list.\n" + + "\tThis may make a subsequent onRemoved( {} ) call redundant.", + offerId, + oldOfferItem.getHashOfPayload() == null ? "null" : oldOfferItem.getHashOfPayload().getHex(), + oldOfferItem.getOffer().getId()); + } + }); } public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { From 98a5b1722cc27124f8ee8ff2275b834cd2343498 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 25 Aug 2021 10:40:57 -0300 Subject: [PATCH 55/56] Add missing detail to comment --- .../bisq/desktop/main/offer/offerbook/OfferBook.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 18ba22b5098..d4c71b1f2dc 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 @@ -165,10 +165,11 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); - // Remove the candidate only if the candidate's offer payload hash matches the - // onRemoved hashOfPayload parameter. We may receive add/remove messages out of - // order (from api's 'editoffer'), and use the offer payload hash to - // ensure we do not remove an edited offer immediately after it was added. + // Remove the candidate only if the candidate's offer payload is null (after list + // is populated by 'fillOfferBookListItems()'), or the hash matches the onRemoved + // hashOfPayload parameter. We may receive add/remove messages out of order + // (from api's 'editoffer'), and use the offer payload hash to ensure we do not + // remove an edited offer immediately after it was added. if ((candidate.getHashOfPayload() == null || candidate.getHashOfPayload().equals(hashOfPayload))) { // The payload-hash test passed, remove the candidate and print reason. offerBookListItems.remove(candidate); From 84036bd86212d0eab8bd48a1114985c5b87b14e1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 25 Aug 2021 11:09:19 -0300 Subject: [PATCH 56/56] Add TODOs (delete debug statement) The new debug log statements included in this PR help trace add/remove list item actions if problems are seen in the UI's OfferBook, after the API 'editoffer' method is released. They can and should be removed in a future PR if the released API feature proves it did not introduce bugs into the UI. --- .../bisq/desktop/main/offer/offerbook/OfferBook.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 d4c71b1f2dc..4e04817f80a 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 @@ -93,7 +93,7 @@ public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer, hashOfPayload); removeDuplicateItem(newOfferBookListItem); offerBookListItems.add(newOfferBookListItem); // Add replacement. - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("onAdded: Added new offer {}\n" + "\twith newItem.payloadHash: {}", offer.getId(), @@ -123,7 +123,7 @@ private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { .collect(Collectors.toList()); duplicateItems.forEach(oldOfferItem -> { offerBookListItems.remove(oldOfferItem); - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("onAdded: Removed old offer {}\n" + "\twith payload hash {} from list.\n" + "\tThis may make a subsequent onRemoved( {} ) call redundant.", @@ -139,7 +139,7 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { offer.setState(Offer.State.REMOVED); offer.cancelAvailabilityRequest(); - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("onRemoved: id = {}\n" + "\twith payload-hash = {}", offer.getId(), @@ -155,7 +155,7 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { .findAny(); if (!candidateWithMatchingPayloadHash.isPresent()) { - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("UI view list does not contain offer with id {} and payload-hash {}", offer.getId(), hashOfPayload == null ? "null" : hashOfPayload.getHex()); @@ -174,14 +174,14 @@ public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) { // The payload-hash test passed, remove the candidate and print reason. offerBookListItems.remove(candidate); - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. log.debug("Candidate.payload-hash: {} is null or == onRemoved.payload-hash: {} ?" + " Yes, removed old offer", candidate.hashOfPayload == null ? "null" : candidate.hashOfPayload.getHex(), hashOfPayload == null ? "null" : hashOfPayload.getHex()); } } else { - if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. // Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash. // Print reason for not removing candidate. log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?"