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

View file

@ -17,19 +17,20 @@
package bisq.core.api;
import bisq.core.api.model.MarketPriceInfo;
import bisq.core.locale.CurrencyUtil;
import bisq.core.provider.price.PriceFeedService;
import javax.inject.Inject;
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 static bisq.common.util.MathUtils.roundDouble;
import static bisq.core.locale.CurrencyUtil.isFiatCurrency;
import static java.lang.String.format;
@Singleton
@Slf4j
class CorePriceService {
@ -41,29 +42,41 @@ class CorePriceService {
this.priceFeedService = priceFeedService;
}
public void getMarketPrice(String currencyCode, Consumer<Double> resultHandler) {
String upperCaseCurrencyCode = currencyCode.toUpperCase();
if (!isFiatCurrency(upperCaseCurrencyCode))
throw new IllegalStateException(format("%s is not a valid currency code", upperCaseCurrencyCode));
if (!priceFeedService.hasPrices())
throw new IllegalStateException("price feed service has no prices");
try {
priceFeedService.setCurrencyCode(upperCaseCurrencyCode);
} catch (Throwable throwable) {
log.warn("Could not set currency code in PriceFeedService", throwable);
/**
* @return Price per 1 XMR in the given currency (fiat or crypto)
*/
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
var marketPrice = priceFeedService.requestAllPrices().get(currencyCode);
if (marketPrice == null) {
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
}
return mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
}
priceFeedService.requestPriceFeed(price -> {
if (price > 0) {
log.info("{} price feed request returned {}", upperCaseCurrencyCode, price);
resultHandler.accept(roundDouble(price, 4));
} else {
throw new IllegalStateException(format("%s price is not available", upperCaseCurrencyCode));
}
},
(errorMessage, throwable) -> log.warn(errorMessage, throwable));
/**
* @return Price per 1 XMR in all supported currencies (fiat & crypto)
*/
public List<MarketPriceInfo> getMarketPrices() throws ExecutionException, InterruptedException, TimeoutException {
return priceFeedService.requestAllPrices().values().stream()
.map(marketPrice -> {
double mappedPrice = mapPriceFeedServicePrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
return new MarketPriceInfo(marketPrice.getCurrencyCode(), mappedPrice);
})
.collect(Collectors.toList());
}
/**
* 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.Random;
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 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
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -70,19 +70,9 @@ public class PriceProvider extends HttpClientProvider {
// get btc per xmr price to convert all prices to xmr
// TODO (woodser): currently using bisq price feed, switch?
Double btcPerXmr = null;
List<?> list = (ArrayList<?>) map.get("data");
double btcPerXmr = findBtcPerXmr(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 {
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
String currencyCode = (String) treeMap.get("currencyCode");
@ -92,8 +82,8 @@ public class PriceProvider extends HttpClientProvider {
// convert price from btc to xmr
boolean isFiat = CurrencyUtil.isFiatCurrency(currencyCode);
if (isFiat) price = price * btcPerXmrFinal;
else price = price / btcPerXmrFinal;
if (isFiat) price = price * btcPerXmr;
else price = price / btcPerXmr;
// add currency price to map
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true));
@ -101,14 +91,28 @@ public class PriceProvider extends HttpClientProvider {
log.error(t.toString());
t.printStackTrace();
}
});
}
// 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");
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() {
return httpClient.getBaseUrl();
}

View file

@ -18,9 +18,12 @@
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.api.model.MarketPriceInfo;
import bisq.proto.grpc.MarketPriceReply;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.MarketPricesReply;
import bisq.proto.grpc.MarketPricesRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
@ -28,6 +31,7 @@ import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
@ -58,17 +62,33 @@ class GrpcPriceService extends PriceImplBase {
public void getMarketPrice(MarketPriceRequest req,
StreamObserver<MarketPriceReply> responseObserver) {
try {
coreApi.getMarketPrice(req.getCurrencyCode(),
price -> {
var reply = MarketPriceReply.newBuilder().setPrice(price).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
double marketPrice = coreApi.getMarketPrice(req.getCurrencyCode());
responseObserver.onNext(MarketPriceReply.newBuilder().setPrice(marketPrice).build());
responseObserver.onCompleted();
} catch (Throwable cause) {
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() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
@ -79,7 +99,7 @@ class GrpcPriceService extends PriceImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
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 {
rpc GetMarketPrice (MarketPriceRequest) returns (MarketPriceReply) {
}
rpc GetMarketPrices (MarketPricesRequest) returns (MarketPricesReply) {
}
}
message MarketPriceRequest {
@ -248,6 +250,18 @@ message MarketPriceReply {
double price = 1;
}
message MarketPricesRequest {
}
message MarketPricesReply {
repeated MarketPriceInfo market_price = 1;
}
message MarketPriceInfo {
string currency_code = 1;
double price = 2;
}
///////////////////////////////////////////////////////////////////////////////////////////
// GetTradeStatistics
///////////////////////////////////////////////////////////////////////////////////////////
@ -371,7 +385,7 @@ message TradeInfo {
bool is_withdrawn = 23;
string contract_as_json = 24;
ContractInfo contract = 25;
string maker_deposit_tx_id = 100;
string taker_deposit_tx_id = 101;
}
@ -389,7 +403,7 @@ message ContractInfo {
string maker_payout_address_string = 10;
string taker_payout_address_string = 11;
uint64 lock_time = 12;
string arbitrator_node_address = 100;
}