From 3c852ed6e83f61ea8d126a7c80ce5fa12f6a394e Mon Sep 17 00:00:00 2001 From: Alessio Date: Thu, 16 Nov 2023 15:59:21 +0100 Subject: [PATCH] #36 Added CoinPaprika oracle Signed-off-by: Alessio --- .../oracle/CoinGeckoCardanoOracle.java | 3 +- .../thothBot/oracle/CoinPaprikaOracle.java | 138 ++++++++++++++++++ .../scheduler/AbstractCheckerTask.java | 4 +- 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/devpool/thothBot/oracle/CoinPaprikaOracle.java diff --git a/src/main/java/com/devpool/thothBot/oracle/CoinGeckoCardanoOracle.java b/src/main/java/com/devpool/thothBot/oracle/CoinGeckoCardanoOracle.java index e335b413..197bc9a8 100644 --- a/src/main/java/com/devpool/thothBot/oracle/CoinGeckoCardanoOracle.java +++ b/src/main/java/com/devpool/thothBot/oracle/CoinGeckoCardanoOracle.java @@ -30,6 +30,7 @@ * See API Documentation */ @Component +@Deprecated(since = "1.4.2", forRemoval = true) public class CoinGeckoCardanoOracle implements ICardanoOracle, Runnable { private static final Logger LOG = LoggerFactory.getLogger(CoinGeckoCardanoOracle.class); private static final String CARDANO_PRICE_DOLLAR_ENDPOINT = "https://api.coingecko.com/api/v3/simple/price?ids=cardano&vs_currencies=usd&precision=4"; @@ -41,7 +42,7 @@ public class CoinGeckoCardanoOracle implements ICardanoOracle, Runnable { private ScheduledExecutorService scheduledExecutorService; - @PostConstruct + //@PostConstruct disabled for now due to easy IP banning public void post() { this.latestCardanoPrice = new AtomicReference<>(); this.latestCardanoPriceUpdateTimestamp = new AtomicLong(-1); diff --git a/src/main/java/com/devpool/thothBot/oracle/CoinPaprikaOracle.java b/src/main/java/com/devpool/thothBot/oracle/CoinPaprikaOracle.java new file mode 100644 index 00000000..19ed253e --- /dev/null +++ b/src/main/java/com/devpool/thothBot/oracle/CoinPaprikaOracle.java @@ -0,0 +1,138 @@ +package com.devpool.thothBot.oracle; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * Oracle implementation based on CoinPaprika. + * See API Documentation + */ +@Component +public class CoinPaprikaOracle implements ICardanoOracle, Runnable { + private static final Logger LOG = LoggerFactory.getLogger(CoinPaprikaOracle.class); + private static final String CARDANO_PRICE_DOLLAR_ENDPOINT = "https://api.coinpaprika.com/v1/coins/ada-cardano/markets?quotes=USD"; + private static final Long CARDANO_PRICE_MAX_AGE = 3600000L; // 1h + + private static final List SELECTED_EXCHANGES_IDS = List.of("coinbase", "kraken", "binance-us", "okx", "bibox", "coinex"); + private static final List SELECTED_PAIRS = List.of("ADA/USD", "ADA/USDT", "ADA/USDC"); + private WebClient webClient; + private AtomicReference latestCardanoPrice; + private AtomicLong latestCardanoPriceUpdateTimestamp; + + private ScheduledExecutorService scheduledExecutorService; + + @PostConstruct + public void post() { + this.latestCardanoPrice = new AtomicReference<>(); + this.latestCardanoPriceUpdateTimestamp = new AtomicLong(-1); + + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) + .responseTimeout(Duration.ofMillis(10000)) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(10000, TimeUnit.MILLISECONDS)) + .addHandlerLast(new WriteTimeoutHandler(10000, TimeUnit.MILLISECONDS))); + + this.webClient = WebClient.builder() + .baseUrl(CARDANO_PRICE_DOLLAR_ENDPOINT) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); + + + this.scheduledExecutorService = Executors.newScheduledThreadPool(1, + new CustomizableThreadFactory("CoinPaprikaThread")); + this.scheduledExecutorService.scheduleWithFixedDelay(this, 1, 900, TimeUnit.SECONDS); + + LOG.info("CoinPaprika Oracle created"); + } + + @PreDestroy + public void shutdown() { + LOG.info("Shutting down executor services"); + this.scheduledExecutorService.shutdown(); + } + + @Override + public void run() { + try { + Mono response = webClient.get() + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(Object.class); + Object data = response.block(); + List> listData = (List>) data; + + if (listData == null) return; + + List> selectedExchanges = listData.stream() + .filter(q -> SELECTED_EXCHANGES_IDS.contains(q.get("exchange_id")) && SELECTED_PAIRS.contains(q.get("pair"))) + .collect(Collectors.toList()); + + double avgPrice = 0.0; + int found = 0; + for (Map selectedExchange : selectedExchanges) { + Map quotes = (Map) selectedExchange.get("quotes"); + if (quotes == null) + continue; + Map priceQuotes = (Map) quotes.get("USD"); + if (priceQuotes == null) + continue; + if (priceQuotes.containsKey("price")) { + avgPrice += priceQuotes.get("price"); + found++; + } + } + + if (found > 0) { + avgPrice = avgPrice / found; + LOG.debug("Computing ADA price using {} quotes from {} exchanges. Price {}", + found, selectedExchanges.size(), avgPrice); + } else { + LOG.warn("Could not find any ADA/USD* price from the data retrieved"); + return; + } + + this.latestCardanoPrice.set(avgPrice); + this.latestCardanoPriceUpdateTimestamp.set(System.currentTimeMillis()); + LOG.debug("Got new Cardano price {} USD", this.latestCardanoPrice.get()); + } catch (WebClientRequestException e) { + LOG.warn("CoinPaprika REST call exception {}", e, e); + } catch (Exception e) { + LOG.error("Unexpected error while getting the cardano price form CoinPaprika", e); + } + } + + @Override + public Double getPriceUsd() { + if (Math.abs(this.latestCardanoPriceUpdateTimestamp.get() - System.currentTimeMillis()) > CARDANO_PRICE_MAX_AGE) { + LOG.warn("The Cardano price {} is older than 1h ({} ms) and therefore not considered valid", + this.latestCardanoPrice.get(), CARDANO_PRICE_MAX_AGE); + return null; + } + return this.latestCardanoPrice.get(); + } +} diff --git a/src/main/java/com/devpool/thothBot/scheduler/AbstractCheckerTask.java b/src/main/java/com/devpool/thothBot/scheduler/AbstractCheckerTask.java index e3a9b5af..a26d726f 100644 --- a/src/main/java/com/devpool/thothBot/scheduler/AbstractCheckerTask.java +++ b/src/main/java/com/devpool/thothBot/scheduler/AbstractCheckerTask.java @@ -3,7 +3,7 @@ import com.devpool.thothBot.dao.UserDao; import com.devpool.thothBot.dao.data.User; import com.devpool.thothBot.koios.KoiosFacade; -import com.devpool.thothBot.oracle.CoinGeckoCardanoOracle; +import com.devpool.thothBot.oracle.CoinPaprikaOracle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -47,7 +47,7 @@ public abstract class AbstractCheckerTask { @Autowired protected KoiosFacade koiosFacade; @Autowired - protected CoinGeckoCardanoOracle oracle; + protected CoinPaprikaOracle oracle; protected String getPoolName(List poolIds, String poolAddress) { if (poolAddress == null) return null;