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.
This commit is contained in:
ghubstan 2021-06-13 12:24:45 -03:00
parent 1daf4715f8
commit 2b8b53bba8
No known key found for this signature in database
GPG key ID: E35592D6800A861E
5 changed files with 303 additions and 85 deletions

View file

@ -21,9 +21,7 @@ import bisq.core.api.model.AddressBalanceInfo;
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.config.Config;
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 java.util.function.Consumer;
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 class CoreApi {
return coreOffersService.getOffer(id);
}
public Offer getMyOffer(String id) {
public OpenOffer getMyOffer(String id) {
return coreOffersService.getMyOffer(id);
}
@ -130,14 +129,10 @@ public class CoreApi {
return coreOffersService.getOffers(direction, currencyCode);
}
public List<Offer> getMyOffers(String direction, String currencyCode) {
public List<OpenOffer> 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 class CoreApi {
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) {

View file

@ -20,13 +20,16 @@ package bisq.core.api;
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.common.util.MathUtils.scaleUpByPowerOf10;
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 @@ import static java.util.Comparator.comparing;
@Slf4j
class CoreOffersService {
private final Supplier<Comparator<Offer>> priceComparator = () -> comparing(Offer::getPrice);
private final Supplier<Comparator<Offer>> reversePriceComparator = () -> comparing(Offer::getPrice).reversed();
private final Supplier<Comparator<Offer>> priceComparator = () ->
comparing(Offer::getPrice);
private final Supplier<Comparator<OpenOffer>> 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 @@ class CoreOffersService {
OfferFilter offerFilter,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
PriceFeedService priceFeedService,
User user) {
this.coreContext = coreContext;
this.keyRing = keyRing;
@ -96,6 +107,7 @@ class CoreOffersService {
this.offerFilter = offerFilter;
this.openOfferManager = openOfferManager;
this.offerUtil = offerUtil;
this.priceFeedService = priceFeedService;
this.user = user;
}
@ -108,10 +120,10 @@ class CoreOffersService {
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 @@ class CoreOffersService {
.collect(Collectors.toList());
}
List<Offer> getMyOffers(String direction, String currencyCode) {
return offerBookService.getOffers().stream()
.filter(o -> o.isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
.sorted(priceComparator(direction))
List<OpenOffer> 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 @@ class CoreOffersService {
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 @@ class CoreOffersService {
}
// 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 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 -> {
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);
}
void cancelOffer(String id) {
Offer offer = getMyOffer(id);
openOfferManager.removeOffer(offer,
OpenOffer openOffer = getMyOffer(id);
openOfferManager.removeOffer(openOffer.getOffer(),
() -> {
},
errorMessage -> {
throw new IllegalStateException(errorMessage);
});
}
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);
}
log::error);
}
private void placeOffer(Offer offer,
@ -252,6 +273,39 @@ class CoreOffersService {
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 @@ class CoreOffersService {
return offerOfWantedDirection && offerInWantedCurrency;
}
private Comparator<OpenOffer> 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<Offer> 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();
}

View file

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

View file

@ -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 class OfferInfo implements Payload {
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 class OfferInfo implements Payload {
.setCounterCurrencyCode(counterCurrencyCode)
.setDate(date)
.setState(state)
.setIsActivated(isActivated)
.build();
}
@ -185,6 +188,7 @@ public class OfferInfo implements Payload {
.withCounterCurrencyCode(proto.getCounterCurrencyCode())
.withDate(proto.getDate())
.withState(proto.getState())
.withIsActivated(proto.getIsActivated())
.build();
}
@ -218,6 +222,7 @@ public class OfferInfo implements Payload {
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 class OfferInfo implements Payload {
return this;
}
public OfferInfoBuilder withIsActivated(boolean isActivated) {
this.isActivated = isActivated;
return this;
}
public OfferInfo build() {
return new OfferInfo(this);
}

View file

@ -26,6 +26,8 @@ import bisq.proto.grpc.CancelOfferReply;
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 @@ class GrpcOffersService extends OffersImplBase {
public void getMyOffer(GetMyOfferRequest req,
StreamObserver<GetMyOfferReply> 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 @@ class GrpcOffersService extends OffersImplBase {
StreamObserver<GetMyOffersReply> responseObserver) {
try {
List<OfferInfo> 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 @@ class GrpcOffersService extends OffersImplBase {
}
}
@Override
public void editOffer(EditOfferRequest req,
StreamObserver<EditOfferReply> 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<CancelOfferReply> responseObserver) {
@ -198,6 +219,7 @@ class GrpcOffersService extends OffersImplBase {
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));
}}
)));