Implement getMarketPrices API endpoint

- Increase rate limit to 10 calls per second.
- Use the new API also for the getMarketPrice call, this makes the 'Can get market prices' API test pass
This commit is contained in:
premek 2021-11-17 15:50:29 +01:00 committed by woodser
parent b1e69f9fdc
commit f27e3e3d1a
7 changed files with 176 additions and 54 deletions

View file

@ -19,6 +19,7 @@ package bisq.core.api;
import bisq.core.api.model.AddressBalanceInfo; import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo; import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.MarketPriceInfo;
import bisq.core.api.model.TxFeeRateInfo; import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
@ -46,6 +47,8 @@ import com.google.common.util.concurrent.FutureCallback;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.Getter; import lombok.Getter;
@ -225,8 +228,12 @@ public class CoreApi {
// Prices // Prices
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void getMarketPrice(String currencyCode, Consumer<Double> resultHandler) { public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException {
corePriceService.getMarketPrice(currencyCode, resultHandler); return corePriceService.getMarketPrice(currencyCode);
}
public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException {
return corePriceService.getMarketPrices();
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -17,19 +17,20 @@
package bisq.core.api; package bisq.core.api;
import bisq.core.api.model.MarketPriceInfo;
import bisq.core.locale.CurrencyUtil;
import bisq.core.provider.price.PriceFeedService; import bisq.core.provider.price.PriceFeedService;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.function.Consumer; import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.common.util.MathUtils.roundDouble;
import static bisq.core.locale.CurrencyUtil.isFiatCurrency;
import static java.lang.String.format;
@Singleton @Singleton
@Slf4j @Slf4j
class CorePriceService { class CorePriceService {
@ -41,29 +42,41 @@ class CorePriceService {
this.priceFeedService = priceFeedService; this.priceFeedService = priceFeedService;
} }
public void getMarketPrice(String currencyCode, Consumer<Double> resultHandler) { /**
String upperCaseCurrencyCode = currencyCode.toUpperCase(); * @return Price per 1 XMR in the given currency (fiat or crypto)
*/
if (!isFiatCurrency(upperCaseCurrencyCode)) public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
throw new IllegalStateException(format("%s is not a valid currency code", upperCaseCurrencyCode)); var marketPrice = priceFeedService.requestAllPrices().get(currencyCode);
if (marketPrice == null) {
if (!priceFeedService.hasPrices()) throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
throw new IllegalStateException("price feed service has no prices"); }
return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
try {
priceFeedService.setCurrencyCode(upperCaseCurrencyCode);
} catch (Throwable throwable) {
log.warn("Could not set currency code in PriceFeedService", throwable);
} }
priceFeedService.requestPriceFeed(price -> { /**
if (price > 0) { * @return Price per 1 XMR in all supported currencies (fiat & crypto)
log.info("{} price feed request returned {}", upperCaseCurrencyCode, price); */
resultHandler.accept(roundDouble(price, 4)); public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException {
} else { return priceFeedService.requestAllPrices().values().stream()
throw new IllegalStateException(format("%s price is not available", upperCaseCurrencyCode)); .map(marketPrice -> {
double mappedPrice = mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
return new MarketPriceInfo(marketPrice.getCurrencyCode(), mappedPrice);
})
.collect(Collectors.toList());
} }
},
(errorMessage, throwable) -> log.warn(errorMessage, throwable)); /**
* PriceProvider returns different values for crypto and fiat,
* e.g. 1 XMR = X USD
* but 1 DOGE = X XMR
* Here we convert all to:
* 1 XMR = X (FIAT or CRYPTO)
*/
private double mapPriceFeedServicePrice(double price, String currencyCode) {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
return price;
}
return price == 0 ? 0 : 1 / price;
// TODO PriceProvider.getAll() could provide these values directly when the original values are not needed for the 'desktop' UI anymore
} }
} }

View file

@ -0,0 +1,48 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.api.model;
import bisq.common.Payload;
import lombok.AllArgsConstructor;
import lombok.ToString;
@ToString
@AllArgsConstructor
public class MarketPriceInfo implements Payload {
private final String currencyCode;
private final double price;
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.MarketPriceInfo toProtoMessage() {
return bisq.proto.grpc.MarketPriceInfo.newBuilder()
.setPrice(price)
.setCurrencyCode(currencyCode)
.build();
}
public static MarketPriceInfo fromProto(bisq.proto.grpc.MarketPriceInfo proto) {
return new MarketPriceInfo(proto.getCurrencyCode(),
proto.getPrice());
}
}

View file

@ -56,6 +56,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -323,6 +327,18 @@ public class PriceFeedService {
}); });
} }
/**
* Returns prices for all available currencies.
* For crypto currencies the value is XMR price for 1 unit of given crypto currency (e.g. 1 DOGE = X XMR).
* For fiat currencies the value is price in the given fiiat currency per 1 XMR (e.g. 1 XMR = X USD).
* Does not update PriceFeedService internal state (cache, epochInMillisAtLastRequest)
*/
public Map<String, MarketPrice> requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException {
return new PriceRequest().requestAllPrices(priceProvider)
.get(20, TimeUnit.SECONDS)
.second;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -70,19 +70,9 @@ public class PriceProvider extends HttpClientProvider {
// get btc per xmr price to convert all prices to xmr // get btc per xmr price to convert all prices to xmr
// TODO (woodser): currently using bisq price feed, switch? // TODO (woodser): currently using bisq price feed, switch?
Double btcPerXmr = null;
List<?> list = (ArrayList<?>) map.get("data"); List<?> list = (ArrayList<?>) map.get("data");
double btcPerXmr = findBtcPerXmr(list);
for (Object obj : list) { for (Object obj : list) {
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
String currencyCode = (String) treeMap.get("currencyCode");
if ("XMR".equalsIgnoreCase(currencyCode)) {
btcPerXmr = (Double) treeMap.get("price");
break;
}
}
final double btcPerXmrFinal = btcPerXmr;
list.forEach(obj -> {
try { try {
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj; LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
String currencyCode = (String) treeMap.get("currencyCode"); String currencyCode = (String) treeMap.get("currencyCode");
@ -92,8 +82,8 @@ public class PriceProvider extends HttpClientProvider {
// convert price from btc to xmr // convert price from btc to xmr
boolean isFiat = CurrencyUtil.isFiatCurrency(currencyCode); boolean isFiat = CurrencyUtil.isFiatCurrency(currencyCode);
if (isFiat) price = price * btcPerXmrFinal; if (isFiat) price = price * btcPerXmr;
else price = price / btcPerXmrFinal; else price = price / btcPerXmr;
// add currency price to map // add currency price to map
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true));
@ -101,14 +91,28 @@ public class PriceProvider extends HttpClientProvider {
log.error(t.toString()); log.error(t.toString());
t.printStackTrace(); t.printStackTrace();
} }
}); }
// add btc to price map, remove xmr since base currency // add btc to price map, remove xmr since base currency
marketPriceMap.put("BTC", new MarketPrice("BTC", 1 / btcPerXmrFinal, marketPriceMap.get("XMR").getTimestampSec(), true)); marketPriceMap.put("BTC", new MarketPrice("BTC", 1 / btcPerXmr, marketPriceMap.get("XMR").getTimestampSec(), true));
marketPriceMap.remove("XMR"); marketPriceMap.remove("XMR");
return new Tuple2<>(tsMap, marketPriceMap); return new Tuple2<>(tsMap, marketPriceMap);
} }
/**
* @return price of 1 XMR in BTC
*/
private static double findBtcPerXmr(List<?> list) {
for (Object obj : list) {
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
String currencyCode = (String) treeMap.get("currencyCode");
if ("XMR".equalsIgnoreCase(currencyCode)) {
return (double) treeMap.get("price");
}
}
throw new IllegalStateException("BTC per XMR price not found");
}
public String getBaseUrl() { public String getBaseUrl() {
return httpClient.getBaseUrl(); return httpClient.getBaseUrl();
} }

View file

@ -18,9 +18,12 @@
package bisq.daemon.grpc; package bisq.daemon.grpc;
import bisq.core.api.CoreApi; import bisq.core.api.CoreApi;
import bisq.core.api.model.MarketPriceInfo;
import bisq.proto.grpc.MarketPriceReply; import bisq.proto.grpc.MarketPriceReply;
import bisq.proto.grpc.MarketPriceRequest; import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.MarketPricesReply;
import bisq.proto.grpc.MarketPricesRequest;
import io.grpc.ServerInterceptor; import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
@ -28,6 +31,7 @@ import io.grpc.stub.StreamObserver;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -58,17 +62,33 @@ class GrpcPriceService extends PriceImplBase {
public void getMarketPrice(MarketPriceRequest req, public void getMarketPrice(MarketPriceRequest req,
StreamObserver<MarketPriceReply> responseObserver) { StreamObserver<MarketPriceReply> responseObserver) {
try { try {
coreApi.getMarketPrice(req.getCurrencyCode(), double marketPrice = coreApi.getMarketPrice(req.getCurrencyCode());
price -> { responseObserver.onNext(MarketPriceReply.newBuilder().setPrice(marketPrice).build());
var reply = MarketPriceReply.newBuilder().setPrice(price).build();
responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
});
} catch (Throwable cause) { } catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver); exceptionHandler.handleException(log, cause, responseObserver);
} }
} }
@Override
public void getMarketPrices(MarketPricesRequest request,
StreamObserver<MarketPricesReply> responseObserver) {
try {
responseObserver.onNext(mapMarketPricesReply(coreApi.getMarketPrices()));
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
private MarketPricesReply mapMarketPricesReply(List<MarketPriceInfo> marketPrices) {
MarketPricesReply.Builder builder = MarketPricesReply.newBuilder();
marketPrices.stream()
.map(MarketPriceInfo::toProtoMessage)
.forEach(builder::addMarketPrice);
return builder.build();
}
final ServerInterceptor[] interceptors() { final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor(); Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor -> return rateMeteringInterceptor.map(serverInterceptor ->
@ -79,7 +99,7 @@ class GrpcPriceService extends PriceImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{ new HashMap<>() {{
put(getGetMarketPriceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetMarketPriceMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
}} }}
))); )));
} }

View file

@ -238,6 +238,8 @@ message GetCryptoCurrencyPaymentMethodsReply {
service Price { service Price {
rpc GetMarketPrice (MarketPriceRequest) returns (MarketPriceReply) { rpc GetMarketPrice (MarketPriceRequest) returns (MarketPriceReply) {
} }
rpc GetMarketPrices (MarketPricesRequest) returns (MarketPricesReply) {
}
} }
message MarketPriceRequest { message MarketPriceRequest {
@ -248,6 +250,18 @@ message MarketPriceReply {
double price = 1; double price = 1;
} }
message MarketPricesRequest {
}
message MarketPricesReply {
repeated MarketPriceInfo market_price = 1;
}
message MarketPriceInfo {
string currency_code = 1;
double price = 2;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// GetTradeStatistics // GetTradeStatistics
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////