diff --git a/build.gradle b/build.gradle index 82d5cc2ffb1..a2a1c3164bd 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ configure(subprojects) { junitVersion = '4.12' jupiterVersion = '5.3.2' kotlinVersion = '1.3.41' - knowmXchangeVersion = '4.3.3' + knowmXchangeVersion = '4.4.2' langVersion = '3.8' logbackVersion = '1.1.11' loggingVersion = '1.2' @@ -458,20 +458,43 @@ configure(project(':pricenode')) { dependencies { compile project(":core") + + compileOnly "org.projectlombok:lombok:$lombokVersion" + annotationProcessor "org.projectlombok:lombok:$lombokVersion" + implementation "com.google.code.gson:gson:$gsonVersion" implementation "commons-codec:commons-codec:$codecVersion" implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion" implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") { exclude(module: 'commons-codec') } - compile("org.knowm.xchange:xchange-bitcoinaverage:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitbay:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-btcmarkets:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-binance:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitfinex:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitflyer:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-bitstamp:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-cexio:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-coinmate:$knowmXchangeVersion") compile("org.knowm.xchange:xchange-coinmarketcap:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-coinone:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-exmo:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-hitbtc:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-huobi:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-independentreserve:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-kraken:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-luno:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-mercadobitcoin:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-paribu:$knowmXchangeVersion") compile("org.knowm.xchange:xchange-poloniex:$knowmXchangeVersion") + compile("org.knowm.xchange:xchange-quoine:$knowmXchangeVersion") compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion") compile("org.springframework.boot:spring-boot-starter-actuator") testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion" testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion" testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + testCompileOnly "org.projectlombok:lombok:$lombokVersion" + testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion" } test { diff --git a/pricenode/README-HEROKU.md b/pricenode/README-HEROKU.md index 19365dc9ef9..ab6081cd96b 100644 --- a/pricenode/README-HEROKU.md +++ b/pricenode/README-HEROKU.md @@ -5,7 +5,6 @@ Run the following commands: heroku create heroku buildpacks:add heroku/gradle - heroku config:set BITCOIN_AVG_PUBKEY=[your pubkey] BITCOIN_AVG_PRIVKEY=[your privkey] git push heroku master curl https://your-app-123456.herokuapp.com/getAllMarketPrices diff --git a/pricenode/README.md b/pricenode/README.md index 55165678d3c..d71dcda6e60 100644 --- a/pricenode/README.md +++ b/pricenode/README.md @@ -24,7 +24,6 @@ Operating a production pricenode is a valuable service to the Bisq network, and To run a pricenode, you will need: - - [BitcoinAverage API keys](https://bitcoinaverage.com/en/plans). Free plans are fine for local development or personal nodes; paid plans should be used for well-known production nodes. - JDK 8 if you want to build and run a node locally. - The `tor` binary (e.g. `brew install tor`) if you want to run a hidden service locally. @@ -40,21 +39,6 @@ curl -s https://raw.githubusercontent.com/bisq-network/bisq/master/pricenode/ins At the end of the installer script, it should print your Tor onion hostname. -### Setting your BitcoinAverage API keys - -Open `/etc/default/bisq-pricenode.env` in a text editor and look for these lines: -```bash -BITCOIN_AVG_PUBKEY=foo -BITCOIN_AVG_PRIVKEY=bar -``` - -Add your pubkey and privkey and then reload/restart bisq-pricenode service: - -```bash -systemctl daemon-reload -systemctl restart bisq-pricenode -``` - ### Test To manually test endpoints, run each of the following: diff --git a/pricenode/bisq-pricenode.env b/pricenode/bisq-pricenode.env index 56a5fe34530..a70fa924c0a 100644 --- a/pricenode/bisq-pricenode.env +++ b/pricenode/bisq-pricenode.env @@ -1,3 +1 @@ -BITCOIN_AVG_PUBKEY=foo -BITCOIN_AVG_PRIVKEY=bar JAVA_OPTS="" diff --git a/pricenode/docker/loop.sh b/pricenode/docker/loop.sh index 1720f6eed90..1382fbb13c5 100644 --- a/pricenode/docker/loop.sh +++ b/pricenode/docker/loop.sh @@ -2,7 +2,7 @@ while true do echo `date` "(Re)-starting node" -BITCOIN_AVG_PUBKEY=$BTCAVERAGE_PUBKEY BITCOIN_AVG_PRIVKEY=$BTCAVERAGE_PRIVKEY java -jar ./build/libs/bisq-pricenode.jar 2 2 +java -jar ./build/libs/bisq-pricenode.jar 2 2 echo `date` "node terminated unexpectedly!!" sleep 3 done diff --git a/pricenode/install_pricenode_debian.sh b/pricenode/install_pricenode_debian.sh index e14033616d4..06cd0f27d99 100755 --- a/pricenode/install_pricenode_debian.sh +++ b/pricenode/install_pricenode_debian.sh @@ -46,7 +46,7 @@ echo "[*] Adding Tor configuration" if ! grep "${BISQ_TORHS}" /etc/tor/torrc >/dev/null 2>&1;then sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${BISQ_TORHS}/ >> ${TOR_CONF}" sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServicePort 80 127.0.0.1:8080 >> ${TOR_CONF}" - sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServiceVersion 2 >> ${TOR_CONF}" + sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONF}" fi echo "[*] Creating Bisq user with Tor access" @@ -60,8 +60,8 @@ echo "[*] Cloning Bisq repo" sudo -H -i -u "${BISQ_USER}" git config --global advice.detachedHead false sudo -H -i -u "${BISQ_USER}" git clone --branch "${BISQ_REPO_TAG}" "${BISQ_REPO_URL}" "${BISQ_HOME}/${BISQ_REPO_NAME}" -echo "[*] Installing OpenJDK 10.0.2 from Bisq repo" -sudo -H -i -u "${ROOT_USER}" "${BISQ_HOME}/${BISQ_REPO_NAME}/scripts/install_java.sh" +echo "[*] Installing OpenJDK 11" +sudo -H -i -u "${ROOT_USER}" apt-get install -qq -y openjdk-11-jdk echo "[*] Checking out Bisq ${BISQ_LATEST_RELEASE}" sudo -H -i -u "${BISQ_USER}" sh -c "cd ${BISQ_HOME}/${BISQ_REPO_NAME} && git checkout ${BISQ_LATEST_RELEASE}" diff --git a/pricenode/src/main/java/bisq/price/spot/ExchangeRateProvider.java b/pricenode/src/main/java/bisq/price/spot/ExchangeRateProvider.java index 554fb0130ce..b15cd5faedb 100644 --- a/pricenode/src/main/java/bisq/price/spot/ExchangeRateProvider.java +++ b/pricenode/src/main/java/bisq/price/spot/ExchangeRateProvider.java @@ -19,24 +19,54 @@ import bisq.price.PriceProvider; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; + +import org.knowm.xchange.Exchange; +import org.knowm.xchange.ExchangeFactory; +import org.knowm.xchange.currency.Currency; +import org.knowm.xchange.currency.CurrencyPair; +import org.knowm.xchange.dto.marketdata.Ticker; +import org.knowm.xchange.exceptions.ExchangeException; +import org.knowm.xchange.exceptions.NotYetImplementedForExchangeException; +import org.knowm.xchange.service.marketdata.MarketDataService; +import org.knowm.xchange.service.marketdata.params.CurrencyPairsParam; +import org.knowm.xchange.service.marketdata.params.Params; + import java.time.Duration; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Abstract base class for providers of bitcoin {@link ExchangeRate} data. Implementations * are marked with the {@link org.springframework.stereotype.Component} annotation in - * order to be discovered via classpath scanning. Implementations are also marked with the - * {@link org.springframework.core.annotation.Order} annotation to determine their - * precedence over each other in the case of two or more services returning exchange rate - * data for the same currency pair. In such cases, results from the provider with the - * higher order value will take precedence over the provider with a lower value, - * presuming that such providers are being iterated over in an ordered list. + * order to be discovered via classpath scanning. If multiple + * {@link ExchangeRateProvider}s retrieve rates for the same currency, then the + * {@link ExchangeRateService} will average them out and expose an aggregate rate. * - * @see ExchangeRateService#ExchangeRateService(java.util.List) + * @see ExchangeRateService#getAllMarketPrices() */ public abstract class ExchangeRateProvider extends PriceProvider> { + public static final Set SUPPORTED_CRYPTO_CURRENCIES = CurrencyUtil.getAllSortedCryptoCurrencies().stream() + .map(TradeCurrency::getCode) + .collect(Collectors.toSet()); + + public static final Set SUPPORTED_FIAT_CURRENCIES = CurrencyUtil.getAllSortedFiatCurrencies().stream() + .map(TradeCurrency::getCode) + .collect(Collectors.toSet()); + private final String name; private final String prefix; @@ -60,4 +90,196 @@ protected void onRefresh() { .filter(e -> "USD".equals(e.getCurrency()) || "LTC".equals(e.getCurrency())) .forEach(e -> log.info("BTC/{}: {}", e.getCurrency(), e.getPrice())); } + + /** + * @param exchangeClass Class of the {@link Exchange} for which the rates should be + * polled + * @return Exchange rates for Bisq-supported fiat currencies and altcoins in the + * specified {@link Exchange} + * + * @see CurrencyUtil#getAllSortedFiatCurrencies() + * @see CurrencyUtil#getAllSortedCryptoCurrencies() + */ + protected Set doGet(Class exchangeClass) { + Set result = new HashSet(); + + // Initialize XChange objects + Exchange exchange = ExchangeFactory.INSTANCE.createExchange(exchangeClass.getName()); + MarketDataService marketDataService = exchange.getMarketDataService(); + + // Retrieve all currency pairs supported by the exchange + List allCurrencyPairsOnExchange = exchange.getExchangeSymbols(); + + // Find out which currency pairs we are interested in polling ("desired pairs") + // This will be the intersection of: + // 1) the pairs available on the exchange, and + // 2) the pairs Bisq considers relevant / valid + // This will result in two lists of desired pairs (fiat and alts) + + // Find the desired fiat pairs (pair format is BTC-FIAT) + List desiredFiatPairs = allCurrencyPairsOnExchange.stream() + .filter(cp -> cp.base.equals(Currency.BTC)) + .filter(cp -> SUPPORTED_FIAT_CURRENCIES.contains(cp.counter.getCurrencyCode())) + .collect(Collectors.toList()); + + // Find the desired altcoin pairs (pair format is ALT-BTC) + List desiredCryptoPairs = allCurrencyPairsOnExchange.stream() + .filter(cp -> cp.counter.equals(Currency.BTC)) + .filter(cp -> SUPPORTED_CRYPTO_CURRENCIES.contains(cp.base.getCurrencyCode())) + .collect(Collectors.toList()); + + // Retrieve in bulk all tickers offered by the exchange + // The benefits of this approach (vs polling each ticker) are twofold: + // 1) the polling of the exchange is faster (one HTTP call vs several) + // 2) it's easier to stay below any API rate limits the exchange might have + List tickersRetrievedFromExchange = new ArrayList<>(); + try { + tickersRetrievedFromExchange = marketDataService.getTickers(new CurrencyPairsParam() { + + /** + * The {@link MarketDataService#getTickers(Params)} interface requires a + * {@link CurrencyPairsParam} argument when polling for tickers in bulk. + * This parameter is meant to indicate a list of currency pairs for which + * the tickers should be polled. However, the actual implementations for + * the different exchanges differ, for example: + * - some will ignore it (and retrieve all available tickers) + * - some will require it (and will fail if a null or empty list is given) + * - some will properly handle it + * + * We take a simplistic approach, namely: + * - for providers that require such a filter, specify one + * - for all others, do not specify one + * + * We make this distinction using + * {@link ExchangeRateProvider#requiresFilterDuringBulkTickerRetrieval} + * + * @return Filter (list of desired currency pairs) to be used during bulk + * ticker retrieval + */ + @Override + public Collection getCurrencyPairs() { + // If required by the exchange implementation, specify a filter + // (list of pairs which should be retrieved) + if (requiresFilterDuringBulkTickerRetrieval()) { + return Stream.of(desiredFiatPairs, desiredCryptoPairs) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + // Otherwise, specify an empty list, indicating that the API should + // simply return all available tickers + return Collections.emptyList(); + } + }); + + if (tickersRetrievedFromExchange.isEmpty()) { + // If the bulk ticker retrieval went through, but no tickers were + // retrieved, this is a strong indication that this specific exchange + // needs a specific list of pairs given as argument, for bulk retrieval to + // work. See requiresFilterDuringBulkTickerRetrieval() + throw new IllegalArgumentException("No tickers retrieved, " + + "exchange requires explicit filter argument during bulk retrieval?"); + } + } catch (NotYetImplementedForExchangeException e) { + // Thrown when a provider has no marketDataService.getTickers() implementation + // either because the exchange API does not provide it, or because it has not + // been implemented yet in the knowm xchange library + + // In this case (retrieval of bulk tickers is not possible) retrieve the + // tickers one by one + List finalTickersRetrievedFromExchange = tickersRetrievedFromExchange; + Stream.of(desiredFiatPairs, desiredCryptoPairs) + .flatMap(Collection::stream) + .collect(Collectors.toList()) + .forEach(cp -> { + try { + + // This is done in a loop, and can therefore result in a burst + // of API calls. Some exchanges do not allow bursts + // A simplistic solution is to delay every call by 1 second + // TODO Switch to using a more elegant solution (per exchange) + // like ResilienceSpecification (needs knowm xchange libs v5) + if (getMarketDataCallDelay() > 0) { + Thread.sleep(getMarketDataCallDelay()); + } + + Ticker ticker = marketDataService.getTicker(cp); + finalTickersRetrievedFromExchange.add(ticker); + + } catch (IOException | InterruptedException ioException) { + ioException.printStackTrace(); + log.error("Could not query tickers for " + getName(), e); + } + }); + } catch (ExchangeException | // Errors reported by the exchange (rate limit, etc) + IOException | // Errors while trying to connect to the API (timeouts, etc) + // Potential error when integrating new exchange (hints that exchange + // provider implementation needs to overwrite + // requiresFilterDuringBulkTickerRetrieval() and have it return true ) + IllegalArgumentException e) { + // Catch and handle all other possible exceptions + // If there was a problem with polling this exchange, return right away, + // since there are no results to parse and process + log.error("Could not query tickers for provider " + getName(), e); + return result; + } + + // Create an ExchangeRate for each desired currency pair ticker that was retrieved + Predicate isDesiredFiatPair = t -> desiredFiatPairs.contains(t.getCurrencyPair()); + Predicate isDesiredCryptoPair = t -> desiredCryptoPairs.contains(t.getCurrencyPair()); + tickersRetrievedFromExchange.stream() + .filter(isDesiredFiatPair.or(isDesiredCryptoPair)) // Only consider desired pairs + .forEach(t -> { + // All tickers here match all requirements + + // We have two kinds of currency pairs, BTC-FIAT and ALT-BTC + // In the first one, BTC is the first currency of the pair + // In the second type, BTC is listed as the second currency + // Distinguish between the two and create ExchangeRates accordingly + + // In every Bisq ExchangeRate, BTC is one currency in the pair + // Extract the other currency from the ticker, to create ExchangeRates + String otherExchangeRateCurrency; + if (t.getCurrencyPair().base.equals(Currency.BTC)) { + otherExchangeRateCurrency = t.getCurrencyPair().counter.getCurrencyCode(); + } else { + otherExchangeRateCurrency = t.getCurrencyPair().base.getCurrencyCode(); + } + + result.add(new ExchangeRate( + otherExchangeRateCurrency, + t.getLast(), + // Some exchanges do not provide timestamps + t.getTimestamp() == null ? new Date() : t.getTimestamp(), + this.getName() + )); + }); + + return result; + } + + /** + * Specifies optional delay between certain kind of API calls that can result in + * bursts. We want to avoid bursts, because this can cause certain exchanges to + * temporarily restrict access to the pricenode IP. + * + * @return Amount of milliseconds of delay between marketDataService.getTicker calls. + * By default 0, but can be overwritten by each provider. + */ + protected long getMarketDataCallDelay() { + return 0; + } + + /** + * @return Whether or not the bulk retrieval of tickers from the exchange requires an + * explicit filter (list of desired pairs) or not. If true, the + * {@link MarketDataService#getTickers(Params)} call will be constructed and given as + * argument, which acts as a filter indicating for which pairs the ticker should be + * retrieved. If false, {@link MarketDataService#getTickers(Params)} will be called + * with an empty argument, indicating that the API should simply return all available + * tickers on the exchange + */ + protected boolean requiresFilterDuringBulkTickerRetrieval() { + return false; + } } diff --git a/pricenode/src/main/java/bisq/price/spot/ExchangeRateService.java b/pricenode/src/main/java/bisq/price/spot/ExchangeRateService.java index 63a29633b71..6fcf4a6eb2b 100644 --- a/pricenode/src/main/java/bisq/price/spot/ExchangeRateService.java +++ b/pricenode/src/main/java/bisq/price/spot/ExchangeRateService.java @@ -17,20 +17,25 @@ package bisq.price.spot; -import bisq.price.spot.providers.BitcoinAverage; - import org.springframework.stereotype.Service; +import java.math.BigDecimal; + import java.util.ArrayList; import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.OptionalDouble; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static java.util.Arrays.asList; + /** * High-level {@link ExchangeRate} data operations. */ @@ -53,32 +58,105 @@ public ExchangeRateService(List providers) { public Map getAllMarketPrices() { Map metadata = new LinkedHashMap<>(); - Map allExchangeRates = new LinkedHashMap<>(); + Map aggregateExchangeRates = getAggregateExchangeRates(); providers.forEach(p -> { Set exchangeRates = p.get(); + + // Specific metadata fields for specific providers are expected by the client, + // mostly for historical reasons + // Therefore, add metadata fields for all known providers + // Rates are encapsulated in the "data" map below metadata.putAll(getMetadata(p, exchangeRates)); - exchangeRates.forEach(e -> - allExchangeRates.put(e.getCurrency(), e) - ); }); - return new LinkedHashMap() {{ - putAll(metadata); - // Use a sorted list by currency code to make comparision of json data between different - // price nodes easier - List values = new ArrayList<>(allExchangeRates.values()); - values.sort(Comparator.comparing(ExchangeRate::getCurrency)); - put("data", values); - }}; + LinkedHashMap result = new LinkedHashMap<>(); + result.putAll(metadata); + // Use a sorted list by currency code to make comparision of json data between + // different price nodes easier + List values = new ArrayList<>(aggregateExchangeRates.values()); + values.sort(Comparator.comparing(ExchangeRate::getCurrency)); + result.put("data", values); + + return result; + } + + /** + * 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 + * aggregate price = average of retrieved prices. If a single provider has rates for + * the currency, then aggregate price = the rate from that provider. + * + * @return Aggregate {@link ExchangeRate}s based on info from all providers, indexed + * by currency code + */ + private Map getAggregateExchangeRates() { + Map aggregateExchangeRates = new HashMap<>(); + + // Query all providers and collect all exchange rates, grouped by currency code + // key = currency code + // value = list of exchange rates + Map> currencyCodeToExchangeRates = getCurrencyCodeToExchangeRates(); + + // For each currency code, calculate aggregate rate + currencyCodeToExchangeRates.forEach((currencyCode, exchangeRateList) -> { + if (exchangeRateList.isEmpty()) { + // If the map was built incorrectly and this currency points to an empty + // list of rates, skip it + return; + } + + ExchangeRate aggregateExchangeRate; + if (exchangeRateList.size() == 1) { + // If a single provider has rates for this currency, then aggregate = rate + // from that provider + aggregateExchangeRate = exchangeRateList.get(0); + } else { + // If multiple providers have rates for this currency, then + // aggregate = average of the rates + OptionalDouble opt = exchangeRateList.stream().mapToDouble(ExchangeRate::getPrice).average(); + // List size > 1, so opt is always set + double priceAvg = opt.orElseThrow(IllegalStateException::new); + + aggregateExchangeRate = new ExchangeRate( + currencyCode, + BigDecimal.valueOf(priceAvg), + new Date(), // timestamp = time when avg is calculated + "Bisq-Aggregate"); + } + aggregateExchangeRates.put(aggregateExchangeRate.getCurrency(), aggregateExchangeRate); + }); + + return aggregateExchangeRates; + } + + /** + * @return All {@link ExchangeRate}s from all providers, grouped by currency code + */ + private Map> getCurrencyCodeToExchangeRates() { + Map> currencyCodeToExchangeRates = new HashMap<>(); + for (ExchangeRateProvider p : providers) { + for (ExchangeRate exchangeRate : p.get()) { + String currencyCode = exchangeRate.getCurrency(); + if (currencyCodeToExchangeRates.containsKey(currencyCode)) { + List l = new ArrayList<>(currencyCodeToExchangeRates.get(currencyCode)); + l.add(exchangeRate); + currencyCodeToExchangeRates.put(currencyCode, l); + } else { + currencyCodeToExchangeRates.put(currencyCode, asList(exchangeRate)); + } + } + } + return currencyCodeToExchangeRates; } private Map getMetadata(ExchangeRateProvider provider, Set exchangeRates) { Map metadata = new LinkedHashMap<>(); - // In case a provider is not available we still want to deliver the data of the other providers, so we catch - // a possible exception and leave timestamp at 0. The Bisq app will check if the timestamp is in a tolerance - // window and if it is too old it will show that the price is not available. + // In case a provider is not available we still want to deliver the data of the + // other providers, so we catch a possible exception and leave timestamp at 0. The + // Bisq app will check if the timestamp is in a tolerance window and if it is too + // old it will show that the price is not available. long timestamp = 0; try { timestamp = getTimestamp(provider, exchangeRates); @@ -88,10 +166,6 @@ private Map getMetadata(ExchangeRateProvider provider, Set. + */ + +package bisq.price.spot.providers; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.btcmarkets.BTCMarketsExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class BTCMarkets extends ExchangeRateProvider { + + public BTCMarkets() { + super("BTCMARKETS", "btcmarkets", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: AUD + // Supported alts: ETH, LTC + return doGet(BTCMarketsExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Binance.java b/pricenode/src/main/java/bisq/price/spot/providers/Binance.java new file mode 100644 index 00000000000..c94107d1ea8 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Binance.java @@ -0,0 +1,45 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.binance.BinanceExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Binance extends ExchangeRateProvider { + + public Binance() { + super("BINANCE", "binance", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: EUR, GBP, NGN, RUB, TRY, UAH, ZAR + // Supported alts: BEAM, DAI, DASH, DCR, DOGE, ETC, ETH, LTC, NAV, PIVX, XMR, XZC, + // ZEC, ZEN + return doGet(BinanceExchange.class); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Bitbay.java b/pricenode/src/main/java/bisq/price/spot/providers/Bitbay.java new file mode 100644 index 00000000000..32e82b377fb --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Bitbay.java @@ -0,0 +1,44 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.bitbay.BitbayExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Bitbay extends ExchangeRateProvider { + + public Bitbay() { + super("BITBAY", "bitbay", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: EUR, GBP, PLN, USD + // Supported alts: DASH, ETH, LTC + return doGet(BitbayExchange.class); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/BitcoinAverage.java b/pricenode/src/main/java/bisq/price/spot/providers/BitcoinAverage.java index d8150b5a108..b5b04ad09e4 100644 --- a/pricenode/src/main/java/bisq/price/spot/providers/BitcoinAverage.java +++ b/pricenode/src/main/java/bisq/price/spot/providers/BitcoinAverage.java @@ -19,141 +19,38 @@ import bisq.price.spot.ExchangeRate; import bisq.price.spot.ExchangeRateProvider; -import bisq.common.util.Hex; -import org.knowm.xchange.bitcoinaverage.dto.marketdata.BitcoinAverageTicker; -import org.knowm.xchange.bitcoinaverage.dto.marketdata.BitcoinAverageTickers; - -import org.springframework.core.annotation.Order; -import org.springframework.core.env.Environment; -import org.springframework.http.RequestEntity; import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -import com.google.common.base.Charsets; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.time.Duration; -import java.time.Instant; -import java.util.Map; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; /** - * See the BitcoinAverage API documentation at https://apiv2.bitcoinaverage.com/#ticker-data-all + * Stub implementation (similar to #CoinMarketCap) for backward compatibility with legacy + * Bisq clients */ -public abstract class BitcoinAverage extends ExchangeRateProvider { - - /** - * Max number of requests allowed per month on the BitcoinAverage developer plan. - * Note the actual max value is 45,000; we use the more conservative value below to - * ensure we do not exceed it. See https://bitcoinaverage.com/en/plans. - */ - private static final double MAX_REQUESTS_PER_MONTH = 42_514; - - private final RestTemplate restTemplate = new RestTemplate(); - private final String symbolSet; +@Component +class BitcoinAverage extends ExchangeRateProvider { - private String pubKey; - private Mac mac; + public BitcoinAverage() { + // Simulate a deactivated BitcoinAverage provider + // We still need the class to exist and be registered as a provider though, + // because the returned data structure must contain the "btcAverageTs" key + // for backward compatibility with Bisq clients which hardcode that key + super("BA", "btcAverage", Duration.ofMinutes(100)); + } /** - * @param symbolSet "global" or "local"; see https://apiv2.bitcoinaverage.com/#supported-currencies + * @see CoinMarketCap#doGet() + * @return */ - public BitcoinAverage(String name, String prefix, double pctMaxRequests, String symbolSet, Environment env) { - super(name, prefix, refreshIntervalFor(pctMaxRequests)); - this.symbolSet = symbolSet; - this.pubKey = env.getRequiredProperty("BITCOIN_AVG_PUBKEY"); - this.mac = initMac(env.getRequiredProperty("BITCOIN_AVG_PRIVKEY")); - } - @Override public Set doGet() { - return getTickersKeyedByCurrency().entrySet().stream() - .filter(e -> supportedCurrency(e.getKey())) - .map(e -> - new ExchangeRate( - e.getKey(), - e.getValue().getLast(), - e.getValue().getTimestamp(), - this.getName() - ) - ) - .collect(Collectors.toSet()); - } - - private boolean supportedCurrency(String currencyCode) { - // ignore Venezuelan bolivars as the "official" exchange rate is just wishful thinking - // we should use this API with a custom provider instead: http://api.bitcoinvenezuela.com/1 - return !"VEF".equals(currencyCode); - } - - private Map getTickersKeyedByCurrency() { - // go from a map with keys like "BTCUSD", "BTCVEF" - return getTickersKeyedByCurrencyPair().entrySet().stream() - // to a map with keys like "USD", "VEF" - .collect(Collectors.toMap(e -> e.getKey().substring(3), Map.Entry::getValue)); - } - - private Map getTickersKeyedByCurrencyPair() { - return restTemplate.exchange( - RequestEntity - .get(UriComponentsBuilder - .fromUriString("https://apiv2.bitcoinaverage.com/indices/{symbol-set}/ticker/all?crypto=BTC") - .buildAndExpand(symbolSet) - .toUri()) - .header("X-signature", getAuthSignature()) - .build(), - BitcoinAverageTickers.class - ).getBody().getTickers(); - } - - protected String getAuthSignature() { - String payload = String.format("%s.%s", Instant.now().getEpochSecond(), pubKey); - return String.format("%s.%s", payload, Hex.encode(mac.doFinal(payload.getBytes(Charsets.UTF_8)))); - } - - private static Mac initMac(String privKey) { - String algorithm = "HmacSHA256"; - SecretKey secretKey = new SecretKeySpec(privKey.getBytes(Charsets.UTF_8), algorithm); - try { - Mac mac = Mac.getInstance(algorithm); - mac.init(secretKey); - return mac; - } catch (NoSuchAlgorithmException | InvalidKeyException ex) { - throw new RuntimeException(ex); - } - } - - private static Duration refreshIntervalFor(double pctMaxRequests) { - long requestsPerMonth = (long) (MAX_REQUESTS_PER_MONTH * pctMaxRequests); - return Duration.ofDays(31).dividedBy(requestsPerMonth); - } - - - @Component - @Order(1) - public static class Global extends BitcoinAverage { - public Global(Environment env) { - super("BTCA_G", "btcAverageG", 0.3, "global", env); - } - } - - - @Component - @Order(2) - public static class Local extends BitcoinAverage { - public Local(Environment env) { - super("BTCA_L", "btcAverageL", 0.7, "local", env); - } + HashSet exchangeRates = new HashSet<>(); + exchangeRates.add(new ExchangeRate("NON_EXISTING_SYMBOL_BA", 0, 0L, getName())); + return exchangeRates; } } diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Bitfinex.java b/pricenode/src/main/java/bisq/price/spot/providers/Bitfinex.java new file mode 100644 index 00000000000..e958d522320 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Bitfinex.java @@ -0,0 +1,44 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.bitfinex.BitfinexExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Bitfinex extends ExchangeRateProvider { + + public Bitfinex() { + super("BITFINEX", "bitfinex", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: EUR, GBP, JPY, USD + // Supported alts: DAI, ETC, ETH, LTC, XMR, ZEC + return doGet(BitfinexExchange.class); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Bitflyer.java b/pricenode/src/main/java/bisq/price/spot/providers/Bitflyer.java new file mode 100644 index 00000000000..e5a78f78496 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Bitflyer.java @@ -0,0 +1,44 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.bitflyer.BitflyerExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Bitflyer extends ExchangeRateProvider { + + public Bitflyer() { + super("BITFLYER", "bitflyer", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: JPY + // Supported alts: ETH + return doGet(BitflyerExchange.class); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Bitpay.java b/pricenode/src/main/java/bisq/price/spot/providers/Bitpay.java new file mode 100644 index 00000000000..0c44cbc2db2 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Bitpay.java @@ -0,0 +1,102 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; +import bisq.price.util.bitpay.BitpayMarketData; +import bisq.price.util.bitpay.BitpayTicker; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.time.Duration; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; + +@Component +class Bitpay extends ExchangeRateProvider { + + private final RestTemplate restTemplate = new RestTemplate(); + + public Bitpay() { + super("BITPAY", "bitpay", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + + Set result = new HashSet(); + + Predicate isDesiredFiatPair = t -> SUPPORTED_FIAT_CURRENCIES.contains(t.getCode()); + Predicate isDesiredCryptoPair = t -> SUPPORTED_CRYPTO_CURRENCIES.contains(t.getCode()); + + getTickers() + .filter(isDesiredFiatPair.or(isDesiredCryptoPair)) + .forEach(ticker -> { + boolean useInverseRate = false; + if (SUPPORTED_CRYPTO_CURRENCIES.contains(ticker.getCode())) { + // Use inverse rate for alts, because the API returns the + // conversion rate in the opposite direction than what we need + // API returns the BTC/Alt rate, we need the Alt/BTC rate + useInverseRate = true; + } + + BigDecimal rate = ticker.getRate(); + // Find the inverse rate, while using enough decimals to reflect very + // small exchange rates + BigDecimal inverseRate = (rate.compareTo(BigDecimal.ZERO) > 0) ? + BigDecimal.ONE.divide(rate, 8, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + result.add(new ExchangeRate( + ticker.getCode(), + (useInverseRate ? inverseRate : rate), + new Date(), + this.getName() + )); + }); + + return result; + } + + private Stream getTickers() { + BitpayMarketData marketData = restTemplate.exchange( + RequestEntity + .get(UriComponentsBuilder + .fromUriString("https://bitpay.com/rates").build() + .toUri()) + .build(), + new ParameterizedTypeReference() { + } + ).getBody(); + + return Arrays.stream(marketData.getData()); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Bitstamp.java b/pricenode/src/main/java/bisq/price/spot/providers/Bitstamp.java new file mode 100644 index 00000000000..ed130aa15d3 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Bitstamp.java @@ -0,0 +1,44 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.bitstamp.BitstampExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Bitstamp extends ExchangeRateProvider { + + public Bitstamp() { + super("BITSTAMP", "bitstamp", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: EUR, GBP, USD + // Supported alts: ETH, LTC + return doGet(BitstampExchange.class); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/CexIO.java b/pricenode/src/main/java/bisq/price/spot/providers/CexIO.java new file mode 100644 index 00000000000..254eadb9eed --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/CexIO.java @@ -0,0 +1,49 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.cexio.CexIOExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class CexIO extends ExchangeRateProvider { + + public CexIO() { + super("CexIO", "cexio", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: EUR, GBP, RUB, USD + // Supported alts: DASH, ETH, LTC + return doGet(CexIOExchange.class); + } + + @Override + protected boolean requiresFilterDuringBulkTickerRetrieval() { + return true; + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/CoinGecko.java b/pricenode/src/main/java/bisq/price/spot/providers/CoinGecko.java new file mode 100644 index 00000000000..aaed9106963 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/CoinGecko.java @@ -0,0 +1,104 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; +import bisq.price.util.coingecko.CoinGeckoMarketData; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.time.Duration; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Component +class CoinGecko extends ExchangeRateProvider { + + private final RestTemplate restTemplate = new RestTemplate(); + + public CoinGecko() { + super("COINGECKO", "coingecko", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + + // Rate limit for the CoinGecko API is 10 calls each second per IP address + // We retrieve all rates in bulk, so we only make 1 call per provider poll + + Set result = new HashSet(); + + Predicate isDesiredFiatPair = t -> SUPPORTED_FIAT_CURRENCIES.contains(t.getKey()); + Predicate isDesiredCryptoPair = t -> SUPPORTED_CRYPTO_CURRENCIES.contains(t.getKey()); + + getMarketData().getRates().entrySet().stream() + .filter(isDesiredFiatPair.or(isDesiredCryptoPair)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + .forEach((key, ticker) -> { + + boolean useInverseRate = false; + if (SUPPORTED_CRYPTO_CURRENCIES.contains(key)) { + // Use inverse rate for alts, because the API returns the + // conversion rate in the opposite direction than what we need + // API returns the BTC/Alt rate, we need the Alt/BTC rate + useInverseRate = true; + } + + BigDecimal rate = ticker.getValue(); + // Find the inverse rate, while using enough decimals to reflect very + // small exchange rates + BigDecimal inverseRate = (rate.compareTo(BigDecimal.ZERO) > 0) ? + BigDecimal.ONE.divide(rate, 8, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + result.add(new ExchangeRate( + key, + (useInverseRate ? inverseRate : rate), + new Date(), + this.getName() + )); + }); + + return result; + } + + private CoinGeckoMarketData getMarketData() { + return restTemplate.exchange( + RequestEntity + .get(UriComponentsBuilder + .fromUriString("https://api.coingecko.com/api/v3/exchange_rates").build() + .toUri()) + .build(), + new ParameterizedTypeReference() { + } + ).getBody(); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/CoinMarketCap.java b/pricenode/src/main/java/bisq/price/spot/providers/CoinMarketCap.java index 1d99d16e0db..39239c9e97e 100644 --- a/pricenode/src/main/java/bisq/price/spot/providers/CoinMarketCap.java +++ b/pricenode/src/main/java/bisq/price/spot/providers/CoinMarketCap.java @@ -20,7 +20,6 @@ import bisq.price.spot.ExchangeRate; import bisq.price.spot.ExchangeRateProvider; -import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.time.Duration; @@ -32,7 +31,6 @@ * Stub implementation of CoinMarketCap price provider to prevent NullPointerExceptions within legacy clients */ @Component -@Order(3) class CoinMarketCap extends ExchangeRateProvider { public CoinMarketCap() { diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Coinmate.java b/pricenode/src/main/java/bisq/price/spot/providers/Coinmate.java new file mode 100644 index 00000000000..8f059271132 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Coinmate.java @@ -0,0 +1,45 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.coinmate.CoinmateExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Coinmate extends ExchangeRateProvider { + + public Coinmate() { + super("Coinmate", "coinmate", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: CZK, EUR + // Supported alts: DASH, ETH, LTC + return doGet(CoinmateExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Coinone.java b/pricenode/src/main/java/bisq/price/spot/providers/Coinone.java new file mode 100644 index 00000000000..6ae54aef253 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Coinone.java @@ -0,0 +1,45 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.coinone.CoinoneExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Coinone extends ExchangeRateProvider { + + public Coinone() { + super("COINONE", "coinone", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: KRW + // Supported alts: - + return doGet(CoinoneExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Coinpaprika.java b/pricenode/src/main/java/bisq/price/spot/providers/Coinpaprika.java new file mode 100644 index 00000000000..ca0c1e9a79a --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Coinpaprika.java @@ -0,0 +1,101 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; +import bisq.price.util.coinpaprika.CoinpaprikaMarketData; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.time.Duration; + +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Component +class Coinpaprika extends ExchangeRateProvider { + + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * Used to determine the currencies in which the BTC price can be quoted. There seems + * to be no programatic way to retrieve it, so we get the value from the API + * documentation (see "quotes" param decsribed at + * https://api.coinpaprika.com/#operation/getTickersById ). The hardcoded value below + * is the list of allowed values as per the API documentation, but without BTC and ETH + * as it makes no sense to quote the BTC price in them. + */ + private final static String SUPPORTED_CURRENCIES = + ("USD, EUR, PLN, KRW, GBP, CAD, JPY, RUB, TRY, NZD, AUD, CHF, UAH, HKD, " + + "SGD, NGN, PHP, MXN, BRL, THB, CLP, CNY, CZK, DKK, HUF, IDR, ILS," + + "INR, MYR, NOK, PKR, SEK, TWD, ZAR, VND, BOB, COP, PEN, ARS, ISK") + .replace(" ", ""); // Strip any spaces + + public Coinpaprika() { + super("COINPAPRIKA", "coinpaprika", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + + // Single IP address can send less than 10 requests per second + // We make only 1 API call per provider poll, so we're not at risk of reaching it + + Set result = new HashSet(); + + Predicate isDesiredFiatPair = t -> SUPPORTED_FIAT_CURRENCIES.contains(t.getKey()); + + getMarketData().getQuotes().entrySet().stream() + .filter(isDesiredFiatPair) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + .forEach((key, ticker) -> { + + result.add(new ExchangeRate( + key, + ticker.getPrice(), + new Date(), + this.getName() + )); + }); + + return result; + } + + private CoinpaprikaMarketData getMarketData() { + return restTemplate.exchange( + RequestEntity + .get(UriComponentsBuilder + .fromUriString( + "https://api.coinpaprika.com/v1/tickers/btc-bitcoin?quotes=" + + SUPPORTED_CURRENCIES).build() + .toUri()) + .build(), + new ParameterizedTypeReference() { + } + ).getBody(); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Exmo.java b/pricenode/src/main/java/bisq/price/spot/providers/Exmo.java new file mode 100644 index 00000000000..51d0daf5b93 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Exmo.java @@ -0,0 +1,46 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.exmo.ExmoExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Exmo extends ExchangeRateProvider { + + public Exmo() { + // API rate limit = 10 calls / second from the same IP ( see https://exmo.com/en/api ) + super("EXMO", "exmo", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: EUR, PLN, RUB, UAH (Ukrainian hryvnia), USD + // Supported alts: DASH, DOGE, ETC, ETH, LTC, XMR, ZEC + return doGet(ExmoExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Hitbtc.java b/pricenode/src/main/java/bisq/price/spot/providers/Hitbtc.java new file mode 100644 index 00000000000..cd98cff1732 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Hitbtc.java @@ -0,0 +1,46 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.hitbtc.v2.HitbtcExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Hitbtc extends ExchangeRateProvider { + + public Hitbtc() { + super("HITBTC", "hitbtc", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: USD + // Supported alts: AEON, BTM, DASH, DCR, DOGE, EMC, ETC, ETH, GRIN, LTC, NAV, + // PART, XMR, XRC, XZC, ZEC, ZEN + return doGet(HitbtcExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Huobi.java b/pricenode/src/main/java/bisq/price/spot/providers/Huobi.java new file mode 100644 index 00000000000..9514aa0cda5 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Huobi.java @@ -0,0 +1,45 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.huobi.HuobiExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Huobi extends ExchangeRateProvider { + + public Huobi() { + super("HUOBI", "huobi", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: - + // Supported alts: BTM, DASH, DCR, DOGE, ETC, ETH, FAIR, LTC, XMR, XZC, ZEC, ZEN + return doGet(HuobiExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/IndependentReserve.java b/pricenode/src/main/java/bisq/price/spot/providers/IndependentReserve.java new file mode 100644 index 00000000000..d0d6c9604cb --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/IndependentReserve.java @@ -0,0 +1,45 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.independentreserve.IndependentReserveExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class IndependentReserve extends ExchangeRateProvider { + + public IndependentReserve() { + super("IndependentReserve", "independentreserve", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: AUD, NZD (New Zealand Dollar), USD + // Supported alts: - + return doGet(IndependentReserveExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Kraken.java b/pricenode/src/main/java/bisq/price/spot/providers/Kraken.java new file mode 100644 index 00000000000..d28645cfdb5 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Kraken.java @@ -0,0 +1,49 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.kraken.KrakenExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Kraken extends ExchangeRateProvider { + + public Kraken() { + super("KRAKEN", "kraken", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: AUD, CAD, CHF, EUR, GBP, JPY, USD + // Supported alts: DASH, DOGE, ETC, ETH, LTC, XMR, ZEC + return doGet(KrakenExchange.class); + } + + @Override + protected boolean requiresFilterDuringBulkTickerRetrieval() { + return true; + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Luno.java b/pricenode/src/main/java/bisq/price/spot/providers/Luno.java new file mode 100644 index 00000000000..b642fc47ceb --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Luno.java @@ -0,0 +1,52 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.luno.LunoExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Luno extends ExchangeRateProvider { + + public Luno() { + super("LUNO", "luno", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: IDR (Indonesian rupiah), MYR (Malaysian ringgit), + // NGN (Nigerian Naira), ZAR (South African rand) + // Supported alts: - + return doGet(LunoExchange.class); + } + + @Override + protected long getMarketDataCallDelay() { + // Luno allows only 1 MarketData call per second + // (see https://www.luno.com/en/developers/api ) + return 1000; + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/MercadoBitcoin.java b/pricenode/src/main/java/bisq/price/spot/providers/MercadoBitcoin.java new file mode 100644 index 00000000000..d790dd43277 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/MercadoBitcoin.java @@ -0,0 +1,45 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.mercadobitcoin.MercadoBitcoinExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class MercadoBitcoin extends ExchangeRateProvider { + + public MercadoBitcoin() { + super("MercadoBitcoin", "mercadobitcoin", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: BRL (Brazilian Real) + // Supported alts: - + return doGet(MercadoBitcoinExchange.class); + } + +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Paribu.java b/pricenode/src/main/java/bisq/price/spot/providers/Paribu.java new file mode 100644 index 00000000000..b9929a62df3 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Paribu.java @@ -0,0 +1,44 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.paribu.ParibuExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Paribu extends ExchangeRateProvider { + + public Paribu() { + super("PARIBU", "paribu", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: TRY (Turkish Lira) + // Supported alts: - + return doGet(ParibuExchange.class); + } +} diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Poloniex.java b/pricenode/src/main/java/bisq/price/spot/providers/Poloniex.java index 684f867b25e..5d5e2be6c4d 100644 --- a/pricenode/src/main/java/bisq/price/spot/providers/Poloniex.java +++ b/pricenode/src/main/java/bisq/price/spot/providers/Poloniex.java @@ -19,76 +19,26 @@ import bisq.price.spot.ExchangeRate; import bisq.price.spot.ExchangeRateProvider; -import bisq.price.util.Altcoins; -import org.knowm.xchange.currency.Currency; -import org.knowm.xchange.currency.CurrencyPair; -import org.knowm.xchange.poloniex.dto.marketdata.PoloniexMarketData; -import org.knowm.xchange.poloniex.dto.marketdata.PoloniexTicker; +import org.knowm.xchange.poloniex.PoloniexExchange; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.annotation.Order; -import org.springframework.http.RequestEntity; import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; import java.time.Duration; -import java.util.Date; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; @Component -@Order(4) class Poloniex extends ExchangeRateProvider { - private final RestTemplate restTemplate = new RestTemplate(); - public Poloniex() { super("POLO", "poloniex", Duration.ofMinutes(1)); } @Override public Set doGet() { - Date timestamp = new Date(); // Poloniex tickers don't include their own timestamp - - return getTickers() - .filter(t -> t.getCurrencyPair().base.equals(Currency.BTC)) - .filter(t -> Altcoins.ALL_SUPPORTED.contains(t.getCurrencyPair().counter.getCurrencyCode())) - .map(t -> - new ExchangeRate( - t.getCurrencyPair().counter.getCurrencyCode(), - t.getPoloniexMarketData().getLast(), - timestamp, - this.getName() - ) - ) - .collect(Collectors.toSet()); - } - - private Stream getTickers() { - - return getTickersKeyedByCurrencyPair().entrySet().stream() - .map(e -> { - String pair = e.getKey(); - PoloniexMarketData data = e.getValue(); - String[] symbols = pair.split("_"); // e.g. BTC_USD => [BTC, USD] - return new PoloniexTicker(data, new CurrencyPair(symbols[0], symbols[1])); - }); - } - - private Map getTickersKeyedByCurrencyPair() { - return restTemplate.exchange( - RequestEntity - .get(UriComponentsBuilder - .fromUriString("https://poloniex.com/public?command=returnTicker").build() - .toUri()) - .build(), - new ParameterizedTypeReference>() { - } - ).getBody(); + // Supported fiat: - + // Supported alts: DASH, DCR, DOGE, ETC, ETH, LTC, XMR, ZEC + return doGet(PoloniexExchange.class); } } diff --git a/pricenode/src/main/java/bisq/price/spot/providers/Quoine.java b/pricenode/src/main/java/bisq/price/spot/providers/Quoine.java new file mode 100644 index 00000000000..e04a76798b5 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/spot/providers/Quoine.java @@ -0,0 +1,49 @@ +/* + * 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; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import org.knowm.xchange.quoine.QuoineExchange; + +import org.springframework.stereotype.Component; + +import java.time.Duration; + +import java.util.Set; + +@Component +class Quoine extends ExchangeRateProvider { + + public Quoine() { + super("QUOINE", "quoine", Duration.ofMinutes(1)); + } + + @Override + public Set doGet() { + // Supported fiat: AUD, CNY, EUR, HKD, IDR, INR, JPY, PHP, SGD, USD + // Supported alts: ETH + return doGet(QuoineExchange.class); + } + + @Override + protected boolean requiresFilterDuringBulkTickerRetrieval() { + return true; + } +} diff --git a/pricenode/src/main/java/bisq/price/util/bitpay/BitpayMarketData.java b/pricenode/src/main/java/bisq/price/util/bitpay/BitpayMarketData.java new file mode 100644 index 00000000000..73ab946a9bc --- /dev/null +++ b/pricenode/src/main/java/bisq/price/util/bitpay/BitpayMarketData.java @@ -0,0 +1,13 @@ +package bisq.price.util.bitpay; + + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BitpayMarketData { + + private BitpayTicker[] data; + +} diff --git a/pricenode/src/main/java/bisq/price/util/bitpay/BitpayTicker.java b/pricenode/src/main/java/bisq/price/util/bitpay/BitpayTicker.java new file mode 100644 index 00000000000..8cc63773ac3 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/util/bitpay/BitpayTicker.java @@ -0,0 +1,18 @@ +package bisq.price.util.bitpay; + +import java.math.BigDecimal; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BitpayTicker { + + private String code; + + private String name; + + private BigDecimal rate; + +} diff --git a/pricenode/src/main/java/bisq/price/util/coingecko/CoinGeckoMarketData.java b/pricenode/src/main/java/bisq/price/util/coingecko/CoinGeckoMarketData.java new file mode 100644 index 00000000000..b96ba10dcaa --- /dev/null +++ b/pricenode/src/main/java/bisq/price/util/coingecko/CoinGeckoMarketData.java @@ -0,0 +1,22 @@ +package bisq.price.util.coingecko; + + +import java.util.Map; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CoinGeckoMarketData { + + private Map rates; + + public void setRates(Map rates) { + // Convert keys to uppercase ("usd" -> "USD") when deserializing API response + this.rates = rates.entrySet().stream() + .collect(Collectors.toMap(entry -> entry.getKey().toUpperCase(), entry -> entry.getValue())); + } + +} diff --git a/pricenode/src/main/java/bisq/price/util/coingecko/CoinGeckoTicker.java b/pricenode/src/main/java/bisq/price/util/coingecko/CoinGeckoTicker.java new file mode 100644 index 00000000000..2326203a100 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/util/coingecko/CoinGeckoTicker.java @@ -0,0 +1,20 @@ +package bisq.price.util.coingecko; + +import java.math.BigDecimal; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CoinGeckoTicker { + + private String name; + + private String unit; + + private BigDecimal value; + + private String type; + +} diff --git a/pricenode/src/main/java/bisq/price/util/coinpaprika/CoinpaprikaMarketData.java b/pricenode/src/main/java/bisq/price/util/coinpaprika/CoinpaprikaMarketData.java new file mode 100644 index 00000000000..b5e366ddbcb --- /dev/null +++ b/pricenode/src/main/java/bisq/price/util/coinpaprika/CoinpaprikaMarketData.java @@ -0,0 +1,17 @@ +package bisq.price.util.coinpaprika; + + +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CoinpaprikaMarketData { + + // All other json fields can be ignored, we don't need them + + private Map quotes; + +} diff --git a/pricenode/src/main/java/bisq/price/util/coinpaprika/CoinpaprikaTicker.java b/pricenode/src/main/java/bisq/price/util/coinpaprika/CoinpaprikaTicker.java new file mode 100644 index 00000000000..64bb3183120 --- /dev/null +++ b/pricenode/src/main/java/bisq/price/util/coinpaprika/CoinpaprikaTicker.java @@ -0,0 +1,16 @@ +package bisq.price.util.coinpaprika; + +import java.math.BigDecimal; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CoinpaprikaTicker { + + // All other json fields can be ignored, we don't need them + + private BigDecimal price; + +} diff --git a/pricenode/src/test/java/bisq/price/AbstractExchangeRateProviderTest.java b/pricenode/src/test/java/bisq/price/AbstractExchangeRateProviderTest.java new file mode 100644 index 00000000000..a459dc68b4a --- /dev/null +++ b/pricenode/src/test/java/bisq/price/AbstractExchangeRateProviderTest.java @@ -0,0 +1,66 @@ +package bisq.price; + +import bisq.price.spot.ExchangeRate; +import bisq.price.spot.ExchangeRateProvider; + +import com.google.common.collect.Sets; + +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static org.junit.Assert.assertTrue; + +@Slf4j +public abstract class AbstractExchangeRateProviderTest { + + protected void doGet_successfulCall(ExchangeRateProvider exchangeProvider) { + + // Use the XChange library to call the provider API, in order to retrieve the + // exchange rates. If the API call fails, or the response body cannot be parsed, + // the test will fail with an exception + Set retrievedExchangeRates = exchangeProvider.doGet(); + + // Log the valid exchange rates which were retrieved + // Useful when running the tests, to easily identify which exchanges provide + // useful pairs + retrievedExchangeRates.forEach(e -> log.info("Found exchange rate " + e.toString())); + + // Sanity checks + assertTrue(retrievedExchangeRates.size() > 0); + checkProviderCurrencyPairs(retrievedExchangeRates); + } + + /** + * Check that every retrieved currency pair is between BTC and either + * A) a fiat currency on the list of Bisq-supported fiat currencies, or + * B) an altcoin on the list of Bisq-supported altcoins + * + * @param retrievedExchangeRates Exchange rates retrieved from the provider + */ + private void checkProviderCurrencyPairs(Set retrievedExchangeRates) { + Set retrievedRatesCurrencies = retrievedExchangeRates.stream() + .map(ExchangeRate::getCurrency) + .collect(Collectors.toSet()); + + Set supportedFiatCurrenciesRetrieved = ExchangeRateProvider.SUPPORTED_FIAT_CURRENCIES.stream() + .filter(f -> retrievedRatesCurrencies.contains(f)) + .collect(Collectors.toCollection(TreeSet::new)); + log.info("Retrieved rates for supported fiat currencies: " + supportedFiatCurrenciesRetrieved); + + Set supportedCryptoCurrenciesRetrieved = ExchangeRateProvider.SUPPORTED_CRYPTO_CURRENCIES.stream() + .filter(c -> retrievedRatesCurrencies.contains(c)) + .collect(Collectors.toCollection(TreeSet::new)); + log.info("Retrieved rates for supported altcoins: " + supportedCryptoCurrenciesRetrieved); + + Set supportedCurrencies = Sets.union( + ExchangeRateProvider.SUPPORTED_CRYPTO_CURRENCIES, + ExchangeRateProvider.SUPPORTED_FIAT_CURRENCIES); + + Set unsupportedCurrencies = Sets.difference(retrievedRatesCurrencies, supportedCurrencies); + assertTrue("Retrieved exchange rates contain unsupported currencies: " + unsupportedCurrencies, + unsupportedCurrencies.isEmpty()); + } +} diff --git a/pricenode/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java b/pricenode/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java index 43473974647..61d2ffbee6a 100644 --- a/pricenode/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java +++ b/pricenode/src/test/java/bisq/price/spot/ExchangeRateServiceTest.java @@ -17,15 +17,22 @@ package bisq.price.spot; +import com.google.common.collect.Sets; + import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomUtils; import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.OptionalDouble; import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.LoggerFactory; @@ -74,8 +81,7 @@ public void getAllMarketPrices_withNoExchangeRates_logs_Exception() { Map retrievedData = service.getAllMarketPrices(); - doSanityChecksForRetrievedDataSingleProvider( - retrievedData, dummyProvider.getPrefix(), numberOfCurrencyPairsOnExchange); + doSanityChecksForRetrievedDataSingleProvider(retrievedData, dummyProvider, numberOfCurrencyPairsOnExchange); // No exchange rates provided by this exchange, two things should happen // A) the timestamp should be set to 0 @@ -101,15 +107,14 @@ public void getAllMarketPrices_withSingleExchangeRate() { Map retrievedData = service.getAllMarketPrices(); - doSanityChecksForRetrievedDataSingleProvider( - retrievedData, dummyProvider.getPrefix(), numberOfCurrencyPairsOnExchange); + doSanityChecksForRetrievedDataSingleProvider(retrievedData, dummyProvider, numberOfCurrencyPairsOnExchange); // One rate was provided by this provider, so the timestamp should not be 0 assertNotEquals(0L, retrievedData.get(dummyProvider.getPrefix() + "Ts")); } @Test - public void getAllMarketPrices_withMultipleProviders() { + public void getAllMarketPrices_withMultipleProviders_differentCurrencyCodes() { int numberOfCurrencyPairsOnExchange = 1; ExchangeRateProvider dummyProvider1 = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange); ExchangeRateProvider dummyProvider2 = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange); @@ -117,8 +122,7 @@ public void getAllMarketPrices_withMultipleProviders() { Map retrievedData = service.getAllMarketPrices(); - doSanityChecksForRetrievedDataMultipleProviders(retrievedData, - asList(dummyProvider1.getPrefix(), dummyProvider2.getPrefix())); + doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(dummyProvider1, dummyProvider2)); // One rate was provided by each provider in this service, so the timestamp // (for both providers) should not be 0 @@ -126,21 +130,49 @@ public void getAllMarketPrices_withMultipleProviders() { assertNotEquals(0L, retrievedData.get(dummyProvider2.getPrefix() + "Ts")); } + /** + * Tests the scenario when multiple providers have rates for the same currencies + */ + @Test + public void getAllMarketPrices_withMultipleProviders_overlappingCurrencyCodes() { + + // List of currencies for which multiple providers will have exchange rates + Set rateCurrencyCodes = Sets.newHashSet("CURRENCY-1", "CURRENCY-2", "CURRENCY-3"); + + // Create several dummy providers, each providing their own rates for the same set of currencies + ExchangeRateProvider dummyProvider1 = buildDummyExchangeRateProvider(rateCurrencyCodes); + ExchangeRateProvider dummyProvider2 = buildDummyExchangeRateProvider(rateCurrencyCodes); + + ExchangeRateService service = new ExchangeRateService(asList(dummyProvider1, dummyProvider2)); + + Map retrievedData = service.getAllMarketPrices(); + + doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(dummyProvider1, dummyProvider2)); + + // At least one rate was provided by each provider in this service, so the + // timestamp (for both providers) should not be 0 + assertNotEquals(0L, retrievedData.get(dummyProvider1.getPrefix() + "Ts")); + assertNotEquals(0L, retrievedData.get(dummyProvider2.getPrefix() + "Ts")); + } + /** * Performs generic sanity checks on the response format and contents. * * @param retrievedData Response data retrieved from the {@link ExchangeRateService} - * @param providerPrefix {@link ExchangeRateProvider#getPrefix()} - * @param numberOfCurrencyPairsOnExchange Number of currency pairs this exchange was initiated with + * @param provider {@link ExchangeRateProvider} available to the + * {@link ExchangeRateService} + * @param numberOfCurrencyPairsOnExchange Number of currency pairs this exchange was + * initiated with */ private void doSanityChecksForRetrievedDataSingleProvider(Map retrievedData, - String providerPrefix, + ExchangeRateProvider provider, int numberOfCurrencyPairsOnExchange) { // Check response structure - doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(providerPrefix)); + doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(provider)); // Check that the amount of provided exchange rates matches expected value - // For one provider, the amount of rates of that provider should be the total amount of rates in the response + // For one provider, the amount of rates of that provider should be the total + // amount of rates in the response List retrievedMarketPricesData = (List) retrievedData.get("data"); assertEquals(numberOfCurrencyPairsOnExchange, retrievedMarketPricesData.size()); } @@ -149,26 +181,81 @@ private void doSanityChecksForRetrievedDataSingleProvider(Map re * Performs generic sanity checks on the response format and contents. * * @param retrievedData Response data retrieved from the {@link ExchangeRateService} - * @param providerPrefixes List of all {@link ExchangeRateProvider#getPrefix()} the + * @param providers List of all {@link ExchangeRateProvider#getPrefix()} the * {@link ExchangeRateService} uses */ private void doSanityChecksForRetrievedDataMultipleProviders(Map retrievedData, - List providerPrefixes) { + List providers) { // Check the correct amount of entries were present in the service response: // The timestamp and the count fields are per provider, so N providers means N // times those fields timestamp (x N) + count (x N) + price data (stored as a list // under the key "data"). So expected size is Nx2 + 1. - int n = providerPrefixes.size(); + int n = providers.size(); assertEquals(n * 2 + 1, retrievedData.size()); - for (String providerPrefix : providerPrefixes) { + for (ExchangeRateProvider provider : providers) { + String providerPrefix = provider.getPrefix(); assertNotNull(retrievedData.get(providerPrefix + "Ts")); assertNotNull(retrievedData.get(providerPrefix + "Count")); } - assertNotNull(retrievedData.get("data")); - // TODO Add checks for the case when rates for the same currency pair is retrieved from multiple providers + // Check validity of the data field + List retrievedRates = (List) retrievedData.get("data"); + assertNotNull(retrievedRates); + + // It should contain no duplicate ExchangeRate objects + int uniqueRates = Sets.newHashSet(retrievedRates).size(); + int totalRates = retrievedRates.size(); + assertEquals(uniqueRates, totalRates, "Found duplicate rates in data field"); + + // There should be only one ExchangeRate per currency + // In other words, even if multiple providers return rates for the same currency, + // the ExchangeRateService should expose only one (aggregate) ExchangeRate for + // that currency + Map currencyCodeToExchangeRateFromService = retrievedRates.stream() + .collect(Collectors.toMap( + ExchangeRate::getCurrency, exchangeRate -> exchangeRate + )); + int uniqueCurrencyCodes = currencyCodeToExchangeRateFromService.keySet().size(); + assertEquals(uniqueCurrencyCodes, uniqueRates, "Found currency code with multiple exchange rates"); + + // Collect all ExchangeRates from all providers and group them by currency code + Map> currencyCodeToExchangeRatesFromProviders = new HashMap<>(); + for (ExchangeRateProvider p : providers) { + for (ExchangeRate exchangeRate : p.get()) { + String currencyCode = exchangeRate.getCurrency(); + if (currencyCodeToExchangeRatesFromProviders.containsKey(currencyCode)) { + List l = new ArrayList<>(currencyCodeToExchangeRatesFromProviders.get(currencyCode)); + l.add(exchangeRate); + currencyCodeToExchangeRatesFromProviders.put(currencyCode, l); + } else { + currencyCodeToExchangeRatesFromProviders.put(currencyCode, asList(exchangeRate)); + } + } + } + + // For each ExchangeRate which is covered by multiple providers, ensure the rate + // 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"); + }); } + /** + * @param numberOfRatesAvailable Number of exchange rates this provider returns + * @return Dummy {@link ExchangeRateProvider} providing rates for + * "numberOfRatesAvailable" random currency codes + */ private ExchangeRateProvider buildDummyExchangeRateProvider(int numberOfRatesAvailable) { ExchangeRateProvider dummyProvider = new ExchangeRateProvider( "ExchangeName-" + getRandomAlphaNumericString(5), @@ -187,8 +274,44 @@ protected Set doGet() { // Simulate the required amount of rates for (int i = 0; i < numberOfRatesAvailable; i++) { exchangeRates.add(new ExchangeRate( - "DUM-" + getRandomAlphaNumericString(3), // random symbol, avoid duplicates - 0, + // random symbol, avoid duplicates + "DUM-" + getRandomAlphaNumericString(3), + RandomUtils.nextDouble(1, 1000), // random price + System.currentTimeMillis(), + getName())); // ExchangeRateProvider name + } + + return exchangeRates; + } + }; + + // Initialize provider + dummyProvider.start(); + dummyProvider.stop(); + + return dummyProvider; + } + + private ExchangeRateProvider buildDummyExchangeRateProvider(Set rateCurrencyCodes) { + ExchangeRateProvider dummyProvider = new ExchangeRateProvider( + "ExchangeName-" + getRandomAlphaNumericString(5), + "EXCH-" + getRandomAlphaNumericString(3), + Duration.ofDays(1)) { + + @Override + public boolean isRunning() { + return true; + } + + @Override + protected Set doGet() { + HashSet exchangeRates = new HashSet<>(); + + // Simulate the required amount of rates + for (String rateCurrencyCode : rateCurrencyCodes) { + exchangeRates.add(new ExchangeRate( + rateCurrencyCode, + RandomUtils.nextDouble(1, 1000), // random price System.currentTimeMillis(), getName())); // ExchangeRateProvider name } diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BTCMarketsTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BTCMarketsTest.java new file mode 100644 index 00000000000..1bd1278e56a --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BTCMarketsTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BTCMarketsTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new BTCMarkets()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BinanceTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BinanceTest.java new file mode 100644 index 00000000000..2a72e67a713 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BinanceTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BinanceTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Binance()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BitbayTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BitbayTest.java new file mode 100644 index 00000000000..fc02ee7a628 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BitbayTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BitbayTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Bitbay()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BitfinexTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BitfinexTest.java new file mode 100644 index 00000000000..a653b6484af --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BitfinexTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BitfinexTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Bitfinex()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BitflyerTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BitflyerTest.java new file mode 100644 index 00000000000..e43d192b366 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BitflyerTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BitflyerTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Bitflyer()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BitpayTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BitpayTest.java new file mode 100644 index 00000000000..38569e1ce84 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BitpayTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BitpayTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Bitpay()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/BitstampTest.java b/pricenode/src/test/java/bisq/price/spot/providers/BitstampTest.java new file mode 100644 index 00000000000..69e3cba6041 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/BitstampTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class BitstampTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Bitstamp()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/CexIOTest.java b/pricenode/src/test/java/bisq/price/spot/providers/CexIOTest.java new file mode 100644 index 00000000000..cf9dc6dcbe0 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/CexIOTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class CexIOTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new CexIO()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/CoinGeckoTest.java b/pricenode/src/test/java/bisq/price/spot/providers/CoinGeckoTest.java new file mode 100644 index 00000000000..43a1a865c87 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/CoinGeckoTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class CoinGeckoTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new CoinGecko()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/CoinmateTest.java b/pricenode/src/test/java/bisq/price/spot/providers/CoinmateTest.java new file mode 100644 index 00000000000..fa8bd1f73f8 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/CoinmateTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class CoinmateTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Coinmate()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/CoinoneTest.java b/pricenode/src/test/java/bisq/price/spot/providers/CoinoneTest.java new file mode 100644 index 00000000000..c722c188acd --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/CoinoneTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class CoinoneTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Coinone()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/CoinpaprikaTest.java b/pricenode/src/test/java/bisq/price/spot/providers/CoinpaprikaTest.java new file mode 100644 index 00000000000..27eb4a642b7 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/CoinpaprikaTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class CoinpaprikaTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Coinpaprika()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/ExmoTest.java b/pricenode/src/test/java/bisq/price/spot/providers/ExmoTest.java new file mode 100644 index 00000000000..68148f45b39 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/ExmoTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class ExmoTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Exmo()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/HitbtcTest.java b/pricenode/src/test/java/bisq/price/spot/providers/HitbtcTest.java new file mode 100644 index 00000000000..0d70e427225 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/HitbtcTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class HitbtcTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Hitbtc()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/HuobiTest.java b/pricenode/src/test/java/bisq/price/spot/providers/HuobiTest.java new file mode 100644 index 00000000000..2ad87856f8e --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/HuobiTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class HuobiTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Huobi()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/IndependentReserveTest.java b/pricenode/src/test/java/bisq/price/spot/providers/IndependentReserveTest.java new file mode 100644 index 00000000000..ccf7cb8b873 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/IndependentReserveTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class IndependentReserveTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new IndependentReserve()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/KrakenTest.java b/pricenode/src/test/java/bisq/price/spot/providers/KrakenTest.java new file mode 100644 index 00000000000..6f8a84a5ca3 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/KrakenTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class KrakenTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Kraken()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/LunoTest.java b/pricenode/src/test/java/bisq/price/spot/providers/LunoTest.java new file mode 100644 index 00000000000..eaa95115778 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/LunoTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class LunoTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Luno()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/MercadoBitcoinTest.java b/pricenode/src/test/java/bisq/price/spot/providers/MercadoBitcoinTest.java new file mode 100644 index 00000000000..92585acb28b --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/MercadoBitcoinTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class MercadoBitcoinTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new MercadoBitcoin()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/ParibuTest.java b/pricenode/src/test/java/bisq/price/spot/providers/ParibuTest.java new file mode 100644 index 00000000000..148a1291cb0 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/ParibuTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class ParibuTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Paribu()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/PoloniexTest.java b/pricenode/src/test/java/bisq/price/spot/providers/PoloniexTest.java new file mode 100644 index 00000000000..70cb3b7d7ca --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/PoloniexTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class PoloniexTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Poloniex()); + } + +} diff --git a/pricenode/src/test/java/bisq/price/spot/providers/QuoineTest.java b/pricenode/src/test/java/bisq/price/spot/providers/QuoineTest.java new file mode 100644 index 00000000000..2067453cce8 --- /dev/null +++ b/pricenode/src/test/java/bisq/price/spot/providers/QuoineTest.java @@ -0,0 +1,34 @@ +/* + * 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; + +import bisq.price.AbstractExchangeRateProviderTest; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.jupiter.api.Test; + +@Slf4j +public class QuoineTest extends AbstractExchangeRateProviderTest { + + @Test + public void doGet_successfulCall() { + doGet_successfulCall(new Quoine()); + } + +}