From f27e3e3d1aff32fc77bbbda80e07ec9536d48ccb Mon Sep 17 00:00:00 2001 From: premek <1145361+premek@users.noreply.github.com> Date: Wed, 17 Nov 2021 15:50:29 +0100 Subject: [PATCH] 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 --- core/src/main/java/bisq/core/api/CoreApi.java | 13 +++- .../java/bisq/core/api/CorePriceService.java | 67 +++++++++++-------- .../bisq/core/api/model/MarketPriceInfo.java | 48 +++++++++++++ .../core/provider/price/PriceFeedService.java | 16 +++++ .../core/provider/price/PriceProvider.java | 34 +++++----- .../bisq/daemon/grpc/GrpcPriceService.java | 34 ++++++++-- proto/src/main/proto/grpc.proto | 18 ++++- 7 files changed, 176 insertions(+), 54 deletions(-) create mode 100644 core/src/main/java/bisq/core/api/model/MarketPriceInfo.java diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 54672ad8..32e5a2bc 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -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 resultHandler) { - corePriceService.getMarketPrice(currencyCode, resultHandler); + public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException { + return corePriceService.getMarketPrice(currencyCode); + } + + public List 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(); } diff --git a/core/src/main/java/bisq/core/api/CorePriceService.java b/core/src/main/java/bisq/core/api/CorePriceService.java index 4553689e..d65aab7d 100644 --- a/core/src/main/java/bisq/core/api/CorePriceService.java +++ b/core/src/main/java/bisq/core/api/CorePriceService.java @@ -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 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 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 } } diff --git a/core/src/main/java/bisq/core/api/model/MarketPriceInfo.java b/core/src/main/java/bisq/core/api/model/MarketPriceInfo.java new file mode 100644 index 00000000..98ed2049 --- /dev/null +++ b/core/src/main/java/bisq/core/api/model/MarketPriceInfo.java @@ -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 . + */ + +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()); + } +} diff --git a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java index 0ef06b65..77566073 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/bisq/core/provider/price/PriceFeedService.java @@ -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 requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException { + return new PriceRequest().requestAllPrices(priceProvider) + .get(20, TimeUnit.SECONDS) + .second; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/provider/price/PriceProvider.java b/core/src/main/java/bisq/core/provider/price/PriceProvider.java index 217c2a5d..a1a21ac6 100644 --- a/core/src/main/java/bisq/core/provider/price/PriceProvider.java +++ b/core/src/main/java/bisq/core/provider/price/PriceProvider.java @@ -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(); } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java index bec21b9c..13bfd88e 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcPriceService.java @@ -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 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 responseObserver) { + try { + responseObserver.onNext(mapMarketPricesReply(coreApi.getMarketPrices())); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + private MarketPricesReply mapMarketPricesReply(List marketPrices) { + MarketPricesReply.Builder builder = MarketPricesReply.newBuilder(); + marketPrices.stream() + .map(MarketPriceInfo::toProtoMessage) + .forEach(builder::addMarketPrice); + return builder.build(); + } + final ServerInterceptor[] interceptors() { Optional 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)); }} ))); } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 49383129..ea7c8b8a 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -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; }