mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 19:49:32 +00:00
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:
parent
b1e69f9fdc
commit
f27e3e3d1a
7 changed files with 176 additions and 54 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
48
core/src/main/java/bisq/core/api/model/MarketPriceInfo.java
Normal file
48
core/src/main/java/bisq/core/api/model/MarketPriceInfo.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}}
|
}}
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Reference in a new issue