From 04f37ba9ac6a8bbcdb1db8c87f511f03e7aba505 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Mon, 4 Sep 2023 18:14:14 +0200 Subject: [PATCH] Add redundancy to ARS price node - Implement ExchangeRateTransformer (@alvasw) - Implement BlueLyticsApi (@alvasw) - Implement ArsBlueMarketGapProvider (@alvasw) - Implement ArsBlueRateTransformer (@alvasw) - merge @alvasw improvements - fixed bug lossing prices after merge - fix tests, all green - delete unused code --- .../price/spot/ArsBlueRateTransformer.java | 61 +++++++++ .../bisq/price/spot/ExchangeRateService.java | 90 +++++--------- .../price/spot/ExchangeRateTransformer.java | 26 ++++ .../spot/providers/BlueRateProvider.java | 21 ++++ .../bisq/price/spot/providers/CryptoYa.java | 2 +- .../bluelytics/ArsBlueMarketGapProvider.java | 56 +++++++++ .../price/util/bluelytics/BlueLyticsApi.java | 41 ++++++ .../price/util/bluelytics/BlueLyticsDto.java | 23 ++-- .../util/bluelytics/BlueLyticsService.java | 117 ------------------ .../price/spot/ExchangeRateServiceTest.java | 43 ++++--- 10 files changed, 269 insertions(+), 211 deletions(-) create mode 100644 src/main/java/bisq/price/spot/ArsBlueRateTransformer.java create mode 100644 src/main/java/bisq/price/spot/ExchangeRateTransformer.java create mode 100644 src/main/java/bisq/price/spot/providers/BlueRateProvider.java create mode 100644 src/main/java/bisq/price/util/bluelytics/ArsBlueMarketGapProvider.java create mode 100644 src/main/java/bisq/price/util/bluelytics/BlueLyticsApi.java delete mode 100644 src/main/java/bisq/price/util/bluelytics/BlueLyticsService.java diff --git a/src/main/java/bisq/price/spot/ArsBlueRateTransformer.java b/src/main/java/bisq/price/spot/ArsBlueRateTransformer.java new file mode 100644 index 0000000..5923256 --- /dev/null +++ b/src/main/java/bisq/price/spot/ArsBlueRateTransformer.java @@ -0,0 +1,61 @@ +/* + * 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.price.spot; + +import bisq.price.spot.providers.BlueRateProvider; +import bisq.price.util.bluelytics.ArsBlueMarketGapProvider; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.OptionalDouble; + +@Component +public class ArsBlueRateTransformer implements ExchangeRateTransformer { + private final ArsBlueMarketGapProvider blueMarketGapProvider; + + public ArsBlueRateTransformer(ArsBlueMarketGapProvider blueMarketGapProvider) { + this.blueMarketGapProvider = blueMarketGapProvider; + } + + @Override + public Optional apply(ExchangeRateProvider provider, ExchangeRate originalExchangeRate) { + if (provider instanceof BlueRateProvider) { + return Optional.of(originalExchangeRate); + } + + OptionalDouble sellGapMultiplier = blueMarketGapProvider.get(); + if (sellGapMultiplier.isEmpty()) { + return Optional.empty(); + } + + double blueRate = originalExchangeRate.getPrice() * sellGapMultiplier.getAsDouble(); + + ExchangeRate newExchangeRate = new ExchangeRate( + originalExchangeRate.getCurrency(), + blueRate, + originalExchangeRate.getTimestamp(), + originalExchangeRate.getProvider() + ); + return Optional.of(newExchangeRate); + } + + @Override + public String supportedCurrency() { + return "ARS"; + } +} diff --git a/src/main/java/bisq/price/spot/ExchangeRateService.java b/src/main/java/bisq/price/spot/ExchangeRateService.java index 8e9f940..93fc290 100644 --- a/src/main/java/bisq/price/spot/ExchangeRateService.java +++ b/src/main/java/bisq/price/spot/ExchangeRateService.java @@ -18,11 +18,7 @@ package bisq.price.spot; import bisq.common.util.Tuple2; - import bisq.core.util.InlierUtil; - -import bisq.price.util.bluelytics.BlueLyticsService; -import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; @@ -40,17 +36,22 @@ class ExchangeRateService { protected final Logger log = LoggerFactory.getLogger(this.getClass()); private final Environment env; private final List providers; + private final List transformers; /** * Construct an {@link ExchangeRateService} with a list of all * {@link ExchangeRateProvider} implementations discovered via classpath scanning. * - * @param providers all {@link ExchangeRateProvider} implementations in ascending - * order of precedence + * @param providers all {@link ExchangeRateProvider} implementations in ascending + * order of precedence + * @param transformers all {@link ExchangeRateTransformer} implementations */ - public ExchangeRateService(Environment env, List providers) { + public ExchangeRateService(Environment env, + List providers, + List transformers) { this.env = env; this.providers = providers; + this.transformers = transformers; } public Map getAllMarketPrices() { @@ -76,56 +77,6 @@ public Map getAllMarketPrices() { return result; } - /** - * Please do not call provider.get(), use this method instead to consider currencies with blue markets - * @param provider to get the rates from - * @return the exchange rates for the different currencies the provider supports considering bluemarket rates if any - */ - public Set providerCurrentExchangeRates(ExchangeRateProvider provider) { - Map blueMarketGapForCurrency = this.getBlueMarketGapForCurrencies(); - Set originalExchangeRates = provider.get(); - if (originalExchangeRates == null) - return null; - - Set exchangeRates = new HashSet<>(); - boolean noOverlapBetweenCurrencies = Sets.intersection(originalExchangeRates.stream().map(ExchangeRate::getCurrency) - .collect(Collectors.toSet()), blueMarketGapForCurrency.keySet()) - .isEmpty(); - - if (provider.alreadyConsidersBlueMarkets() || noOverlapBetweenCurrencies) { - exchangeRates.addAll(originalExchangeRates); - } else { - this.addRatesUpdatingBlueGaps(blueMarketGapForCurrency, provider, exchangeRates, originalExchangeRates); - } - return exchangeRates; - } - - private void addRatesUpdatingBlueGaps(Map blueMarketGapForCurrency, ExchangeRateProvider provider, Set exchangeRates, Set originalExchangeRates) { - originalExchangeRates.forEach(er -> { - // default to original rate - ExchangeRate exchangeRateToUse = er; - if (blueMarketGapForCurrency.containsKey(er.getCurrency())) { - ExchangeRate updatedExchangeRate = provider.maybeUpdateBlueMarketPriceGapForRate(er, blueMarketGapForCurrency.get(er.getCurrency())); - if (updatedExchangeRate != null) { - exchangeRateToUse = updatedExchangeRate; -// this.log.info(String.format("Replaced original %s rate of $%s to $%s", er.getCurrency(), BigDecimal.valueOf(er.getPrice()).toEngineeringString(), BigDecimal.valueOf(updatedExchangeRate.getPrice()).toEngineeringString())); - } - } - exchangeRates.add(exchangeRateToUse); - }); - } - - private Map getBlueMarketGapForCurrencies() { - Map blueMarketGapForCurrencies = new HashMap<>(); - // ARS - Double arsBlueMultiplier = BlueLyticsService.getInstance().blueGapMultiplier(); - if (!arsBlueMultiplier.isNaN()) { -// this.log.info("Updated ARS/USD multiplier is " + arsBlueMultiplier); - blueMarketGapForCurrencies.put("ARS", arsBlueMultiplier); - } - return blueMarketGapForCurrencies; - } - /** * For each currency, create an aggregate {@link ExchangeRate} based on the currency's * rates from all providers. If multiple providers have rates for the currency, then @@ -212,20 +163,39 @@ private double getOutlierStdDeviation() { private Map> getCurrencyCodeToExchangeRates() { Map> currencyCodeToExchangeRates = new HashMap<>(); for (ExchangeRateProvider p : providers) { - Set exchangeRates = providerCurrentExchangeRates(p); + Set exchangeRates = p.get(); if (exchangeRates == null) continue; for (ExchangeRate exchangeRate : exchangeRates) { String currencyCode = exchangeRate.getCurrency(); + + List transformedExchangeRates = transformers.stream() + .filter(transformer -> transformer.supportedCurrency() + .equals(currencyCode) + ) + .map(t -> t.apply(p, exchangeRate)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + if (currencyCodeToExchangeRates.containsKey(currencyCode)) { List l = new ArrayList<>(currencyCodeToExchangeRates.get(currencyCode)); - l.add(exchangeRate); + if (transformedExchangeRates.isEmpty()) { + l.add(exchangeRate); + } else { + l.addAll(transformedExchangeRates); + } currencyCodeToExchangeRates.put(currencyCode, l); } else { - currencyCodeToExchangeRates.put(currencyCode, List.of(exchangeRate)); + if (transformedExchangeRates.isEmpty()) { + currencyCodeToExchangeRates.put(currencyCode, List.of(exchangeRate)); + } else { + currencyCodeToExchangeRates.put(currencyCode, transformedExchangeRates); + } } } } + return currencyCodeToExchangeRates; } diff --git a/src/main/java/bisq/price/spot/ExchangeRateTransformer.java b/src/main/java/bisq/price/spot/ExchangeRateTransformer.java new file mode 100644 index 0000000..d96e8e0 --- /dev/null +++ b/src/main/java/bisq/price/spot/ExchangeRateTransformer.java @@ -0,0 +1,26 @@ +/* + * 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.price.spot; + +import java.util.Optional; + +public interface ExchangeRateTransformer { + Optional apply(ExchangeRateProvider provider, ExchangeRate exchangeRate); + + String supportedCurrency(); +} diff --git a/src/main/java/bisq/price/spot/providers/BlueRateProvider.java b/src/main/java/bisq/price/spot/providers/BlueRateProvider.java new file mode 100644 index 0000000..1d2a0f3 --- /dev/null +++ b/src/main/java/bisq/price/spot/providers/BlueRateProvider.java @@ -0,0 +1,21 @@ +/* + * 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.price.spot.providers; + +public interface BlueRateProvider { +} diff --git a/src/main/java/bisq/price/spot/providers/CryptoYa.java b/src/main/java/bisq/price/spot/providers/CryptoYa.java index 9e471b8..43d1b3c 100644 --- a/src/main/java/bisq/price/spot/providers/CryptoYa.java +++ b/src/main/java/bisq/price/spot/providers/CryptoYa.java @@ -39,7 +39,7 @@ * This ExchangeRateProvider provides a real market rate (black or "blue") for ARS/BTC */ @Component -class CryptoYa extends ExchangeRateProvider { +class CryptoYa extends ExchangeRateProvider implements BlueRateProvider { private static final String CRYPTO_YA_BTC_ARS_API_URL = "https://criptoya.com/api/btc/ars/0.1"; diff --git a/src/main/java/bisq/price/util/bluelytics/ArsBlueMarketGapProvider.java b/src/main/java/bisq/price/util/bluelytics/ArsBlueMarketGapProvider.java new file mode 100644 index 0000000..baa0304 --- /dev/null +++ b/src/main/java/bisq/price/util/bluelytics/ArsBlueMarketGapProvider.java @@ -0,0 +1,56 @@ +/* + * 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.price.util.bluelytics; + +import bisq.price.PriceProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Optional; +import java.util.OptionalDouble; + +@Slf4j +@Component +public class ArsBlueMarketGapProvider extends PriceProvider { + public interface Listener { + void onUpdate(OptionalDouble sellGapMultiplier); + } + + private static final Duration REFRESH_INTERVAL = Duration.ofHours(1); + + private final BlueLyticsApi blueLyticsApi = new BlueLyticsApi(); + private final Optional onUpdateListener; + + public ArsBlueMarketGapProvider() { + super(REFRESH_INTERVAL); + this.onUpdateListener = Optional.empty(); + } + + public ArsBlueMarketGapProvider(Listener onUpdateListener) { + super(REFRESH_INTERVAL); + this.onUpdateListener = Optional.of(onUpdateListener); + } + + @Override + protected OptionalDouble doGet() { + OptionalDouble sellGapMultiplier = blueLyticsApi.getSellGapMultiplier(); + onUpdateListener.ifPresent(listener -> listener.onUpdate(sellGapMultiplier)); + return sellGapMultiplier; + } +} diff --git a/src/main/java/bisq/price/util/bluelytics/BlueLyticsApi.java b/src/main/java/bisq/price/util/bluelytics/BlueLyticsApi.java new file mode 100644 index 0000000..13ebcd8 --- /dev/null +++ b/src/main/java/bisq/price/util/bluelytics/BlueLyticsApi.java @@ -0,0 +1,41 @@ +/* + * 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.price.util.bluelytics; + +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.OptionalDouble; + +public class BlueLyticsApi { + private static final String API_URL = "https://api.bluelytics.com.ar/v2/latest"; + private final WebClient webClient = WebClient.create(); + + public OptionalDouble getSellGapMultiplier() { + BlueLyticsDto blueLyticsDto = webClient.get() + .uri(API_URL) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(BlueLyticsDto.class) + .block(Duration.of(30, ChronoUnit.SECONDS)); + + return blueLyticsDto == null ? OptionalDouble.empty() : blueLyticsDto.gapSellMultiplier(); + } +} diff --git a/src/main/java/bisq/price/util/bluelytics/BlueLyticsDto.java b/src/main/java/bisq/price/util/bluelytics/BlueLyticsDto.java index 4668397..5c41c20 100644 --- a/src/main/java/bisq/price/util/bluelytics/BlueLyticsDto.java +++ b/src/main/java/bisq/price/util/bluelytics/BlueLyticsDto.java @@ -22,6 +22,7 @@ import lombok.Setter; import java.util.Date; +import java.util.OptionalDouble; @Getter @Setter @@ -29,21 +30,17 @@ public class BlueLyticsDto { @Getter @Setter public static class USDRate { - Double value_avg; - Double value_sell; - Double value_buy; + double value_avg; + double value_sell; + double value_buy; } - BlueLyticsDto.USDRate oficial; - BlueLyticsDto.USDRate blue; - Date last_update; + private BlueLyticsDto.USDRate oficial; + private BlueLyticsDto.USDRate blue; + private Date last_update; - /** - * - * @return the sell multiplier to go from oficial to blue market for ARS/USD - * if its not available, returns NaN - */ - public Double gapSellMultiplier() { - return this.blue.value_sell / this.oficial.value_sell; + public OptionalDouble gapSellMultiplier() { + double sellMultiplier = blue.value_sell / oficial.value_sell; + return Double.isNaN(sellMultiplier) ? OptionalDouble.empty() : OptionalDouble.of(sellMultiplier); } } diff --git a/src/main/java/bisq/price/util/bluelytics/BlueLyticsService.java b/src/main/java/bisq/price/util/bluelytics/BlueLyticsService.java deleted file mode 100644 index c72fca3..0000000 --- a/src/main/java/bisq/price/util/bluelytics/BlueLyticsService.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.price.util.bluelytics; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.RequestEntity; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -import java.time.Instant; -import java.util.Date; -import java.util.Objects; - -/** - * Util singleton object to update and provider ARS/USD blue market gap against the oficial rate. - * This is useful for example, to estimate ARS/BTC blue (real) market rate in a country with heavy currency controls - */ -public final class BlueLyticsService { - private static final long MIN_REFRESH_WINDOW = 3600000; // 1hr - private static final String GET_USD_EXCHANGE_RATES_ARG_URL = "https://api.bluelytics.com.ar/v2/latest"; - private static final BlueLyticsService instance = new BlueLyticsService(); - private final Logger logger = LoggerFactory.getLogger(getClass()); - private final RestTemplate restTemplate = new RestTemplate(); - private Double lastBlueGap; - private Long lastRefresh; - private Thread refreshJob; - - private BlueLyticsService() { - lastRefresh = null; - lastBlueGap = null; - refreshJob = null; - } - - public static BlueLyticsService getInstance() { - return BlueLyticsService.instance; - } - - /** - * - * @return current ARS/USD gap multiplier to get from official rate to free market rate. - * If not available returns Nan - */ - public Double blueGapMultiplier() { - maybeLaunchAsyncRefresh(); - return Objects.requireNonNullElse(lastBlueGap, Double.NaN); - } - - /** - * if enough time {@see BlueLyticsUSDRate.MIN_FRESH_WINDOW} has pass from the last refresh or - * no refresh has been done before: launch async refresh - */ - private void maybeLaunchAsyncRefresh() { - long nowTimestamp = Date.from(Instant.now()).getTime(); - if (refreshJob != null) { - logger.info("Skipping ARS/USD multiplier refresh since its already running"); - } else { - if (lastRefresh == null || - nowTimestamp > (lastRefresh + BlueLyticsService.MIN_REFRESH_WINDOW)) { - if (lastRefresh == null) { - logger.info("Refreshing for the first time"); - } else { - logger.info(String.format("should refresh? %s with last refresh %s and now time %s", (lastRefresh + BlueLyticsService.MIN_REFRESH_WINDOW), lastRefresh, nowTimestamp)); - } - launchAsyncRefresh(); - } - } - } - - private synchronized void launchAsyncRefresh() { - logger.info("Launching async refresh of blue ARS/USD rate"); - refreshJob = new Thread(this::refreshBlueGap); - refreshJob.start(); - } - - private void refreshBlueGap() { - try { - // the last_update value is different than the last one and also launch the update if 1 hour passed ? - lastBlueGap = Objects.requireNonNull(restTemplate.exchange( - RequestEntity - .get(UriComponentsBuilder - .fromUriString(BlueLyticsService.GET_USD_EXCHANGE_RATES_ARG_URL).build() - .toUri()) - .build(), - new ParameterizedTypeReference() { - } - ).getBody()).gapSellMultiplier(); - lastRefresh = new Date().getTime(); - logger.info(String.format("New blue gap is %s and refresh was at epoch %s", lastBlueGap, lastRefresh)); - } catch (Exception e) { - logger.error("Failed to fetch updated bluelytics gap multiplier", e); - } finally { - refreshJob = null; - } - } - - @Override - protected Object clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException("Cannot clone Singleton"); - } -} diff --git a/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java b/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java index 96761ff..c9ff1c7 100644 --- a/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java +++ b/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java @@ -18,7 +18,6 @@ package bisq.price.spot; import bisq.core.locale.CurrencyUtil; -import bisq.price.util.bluelytics.BlueLyticsService; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -57,8 +56,6 @@ static void setup() { // Get the logger object for logs in ExchangeRateService exchangeRateServiceLogger = (Logger) LoggerFactory.getLogger(ExchangeRateService.class); exchangeRateServiceLogger.info("Setup started"); - // trigger the first init - BlueLyticsService.getInstance().blueGapMultiplier(); // Initiate and append a ListAppender, which allows us to programmatically inspect // log messages @@ -73,7 +70,8 @@ static void setup() { public void getAllMarketPrices_withNoExchangeRates_logs_Exception() { int numberOfCurrencyPairsOnExchange = 0; ExchangeRateProvider dummyProvider = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange); - ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), Collections.singletonList(dummyProvider)); + ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), + Collections.singletonList(dummyProvider), Collections.emptyList()); Map retrievedData = service.getAllMarketPrices(); @@ -99,7 +97,8 @@ public void getAllMarketPrices_withNoExchangeRates_logs_Exception() { public void getAllMarketPrices_withSingleExchangeRate() { int numberOfCurrencyPairsOnExchange = 1; ExchangeRateProvider dummyProvider = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange); - ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), Collections.singletonList(dummyProvider)); + ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), + Collections.singletonList(dummyProvider), Collections.emptyList()); Map retrievedData = service.getAllMarketPrices(); @@ -114,7 +113,8 @@ public void getAllMarketPrices_withMultipleProviders_differentCurrencyCodes() { int numberOfCurrencyPairsOnExchange = 1; ExchangeRateProvider dummyProvider1 = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange); ExchangeRateProvider dummyProvider2 = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange); - ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), asList(dummyProvider1, dummyProvider2)); + ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), + asList(dummyProvider1, dummyProvider2), Collections.emptyList()); Map retrievedData = service.getAllMarketPrices(); @@ -139,7 +139,7 @@ public void getAllMarketPrices_oneProvider_considerBlueUpdates() { when(mockedEnvironment.getProperty(eq("bisq.price.fiatcurrency.excluded"), anyString())).thenReturn(excludedCcvString); when(mockedEnvironment.getProperty(eq("bisq.price.fiatcurrency.excludedByProvider"), anyString())).thenReturn(providerExcludedCcvString); ExchangeRateProvider dummyProvider = buildDummyExchangeRateProvider(rateCurrencyCodes, mockedEnvironment); - ExchangeRateService service = new ExchangeRateService(mockedEnvironment, List.of(dummyProvider)); + ExchangeRateService service = new ExchangeRateService(mockedEnvironment, List.of(dummyProvider), List.of()); Map retrievedData = service.getAllMarketPrices(); @@ -160,7 +160,8 @@ public void getAllMarketPrices_withMultipleProviders_overlappingCurrencyCodes() ExchangeRateProvider dummyProvider1 = buildDummyExchangeRateProvider(rateCurrencyCodes, null); ExchangeRateProvider dummyProvider2 = buildDummyExchangeRateProvider(rateCurrencyCodes, null); - ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), asList(dummyProvider1, dummyProvider2)); + ExchangeRateService service = new ExchangeRateService(new StandardEnvironment(), + asList(dummyProvider1, dummyProvider2), Collections.emptyList()); Map retrievedData = service.getAllMarketPrices(); @@ -308,7 +309,7 @@ private void doSanityChecksForRetrievedDataMultipleProviders(ExchangeRateService // Collect all ExchangeRates from all providers and group them by currency code Map> currencyCodeToExchangeRatesFromProviders = new HashMap<>(); for (ExchangeRateProvider p : providers) { - for (ExchangeRate exchangeRate : service.providerCurrentExchangeRates(p)) { + for (ExchangeRate exchangeRate : p.get()) { String currencyCode = exchangeRate.getCurrency(); if (currencyCodeToExchangeRatesFromProviders.containsKey(currencyCode)) { List l = new ArrayList<>(currencyCodeToExchangeRatesFromProviders.get(currencyCode)); @@ -324,17 +325,19 @@ private void doSanityChecksForRetrievedDataMultipleProviders(ExchangeRateService // value is an average currencyCodeToExchangeRatesFromProviders.forEach((currencyCode, exchangeRateList) -> { ExchangeRate rateFromService = currencyCodeToExchangeRateFromService.get(currencyCode); - double priceFromService = rateFromService.getPrice(); - - OptionalDouble opt = exchangeRateList.stream().mapToDouble(ExchangeRate::getPrice).average(); - double priceAvgFromProviders = opt.getAsDouble(); - - // Ensure that the ExchangeRateService correctly aggregates exchange rates - // from multiple providers. If multiple providers contain rates for a - // currency, the service should return a single aggregate rate - // Expected value for aggregate rate = avg(provider rates) - // This formula works for any number of providers for a specific currency - assertEquals(priceFromService, priceAvgFromProviders, "Service returned incorrect aggregate rate"); + if (rateFromService != null) { + double priceFromService = rateFromService.getPrice(); + + OptionalDouble opt = exchangeRateList.stream().mapToDouble(ExchangeRate::getPrice).average(); + double priceAvgFromProviders = opt.getAsDouble(); + + // Ensure that the ExchangeRateService correctly aggregates exchange rates + // from multiple providers. If multiple providers contain rates for a + // currency, the service should return a single aggregate rate + // Expected value for aggregate rate = avg(provider rates) + // This formula works for any number of providers for a specific currency + assertEquals(priceFromService, priceAvgFromProviders, "Service returned incorrect aggregate rate"); + } }); }