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
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
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.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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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));
|
||||
}}
|
||||
)));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user