package io.bitsquare.btc.pricefeed;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.google.inject.Inject;
import io.bitsquare.app.AppOptionKeys;
import io.bitsquare.app.Log;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.handlers.FaultHandler;
import io.bitsquare.common.util.Tuple2;
import io.bitsquare.http.HttpClient;
import io.bitsquare.network.NetworkOptionKeys;
import javafx.beans.property.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.inject.Named;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.function.Consumer;
import static com.google.common.base.Preconditions.checkNotNull;
public class PriceFeedService {
private static final Logger log = LoggerFactory.getLogger(PriceFeedService.class);
private HttpClient httpClient;
///////////////////////////////////////////////////////////////////////////////////////////
// Enum
///////////////////////////////////////////////////////////////////////////////////////////
public enum Type {
ASK("Ask"),
BID("Bid"),
LAST("Last");
public final String name;
Type(String name) {
this.name = name;
}
}
private static final long PERIOD_SEC = 60;
private final Map<String, MarketPrice> cache = new HashMap<>();
private PriceProvider priceProvider;
private Consumer<Double> priceConsumer;
private FaultHandler faultHandler;
private Type type;
private String currencyCode;
private final StringProperty currencyCodeProperty = new SimpleStringProperty();
private final ObjectProperty<Type> typeProperty = new SimpleObjectProperty<>();
private final IntegerProperty currenciesUpdateFlag = new SimpleIntegerProperty(0);
private long epochInSecondAtLastRequest;
private Map<String, Long> timeStampMap = new HashMap<>();
private String baseUrl;
private final String[] priceFeedProviderArray;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public PriceFeedService(HttpClient httpClient,
@Named(AppOptionKeys.PRICE_FEED_PROVIDERS) String priceFeedProviders,
@Named(NetworkOptionKeys.USE_LOCALHOST) boolean useLocalhost) {
this.httpClient = httpClient;
if (priceFeedProviders.isEmpty()) {
if (useLocalhost) {
// If we run in localhost mode we don't have the tor node running, so we need a clearnet host
priceFeedProviders = "http://95.85.11.205:8080/";
// Use localhost for using a locally running priceprovider
// priceFeedProviders = "http://localhost:8080/";
} else {
priceFeedProviders = "http://t4wlzy7l6k4hnolg.onion/, http://g27szt7aw2vrtowe.onion/";
}
}
priceFeedProviderArray = priceFeedProviders.replace(" ", "").split(",");
int index = new Random().nextInt(priceFeedProviderArray.length);
baseUrl = priceFeedProviderArray[index];
log.info("baseUrl for PriceFeedService: " + baseUrl);
this.priceProvider = new PriceProvider(httpClient, baseUrl);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void init(Consumer<Double> resultHandler, FaultHandler faultHandler) {
this.priceConsumer = resultHandler;
this.faultHandler = faultHandler;
request();
}
private void request() {
requestAllPrices(priceProvider, () -> {
applyPriceToConsumer();
// after first response we know the providers timestamp and want to request quickly after next expected update
long delay = Math.max(40, Math.min(90, PERIOD_SEC - (Instant.now().getEpochSecond() - epochInSecondAtLastRequest) + 2 + new Random().nextInt(5)));
UserThread.runAfter(this::request, delay);
}, (errorMessage, throwable) -> {
// Try other provider if more then 1 is available
if (priceFeedProviderArray.length > 1) {
String newBaseUrl;
do {
int index = new Random().nextInt(priceFeedProviderArray.length);
newBaseUrl = priceFeedProviderArray[index];
}
while (baseUrl.equals(newBaseUrl));
baseUrl = newBaseUrl;
log.info("Try new baseUrl after error: " + baseUrl);
this.priceProvider = new PriceProvider(httpClient, baseUrl);
request();
} else {
UserThread.runAfter(this::request, 120);
}
this.faultHandler.handleFault(errorMessage, throwable);
});
}
@Nullable
public MarketPrice getMarketPrice(String currencyCode) {
if (cache.containsKey(currencyCode))
return cache.get(currencyCode);
else
return null;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setter
///////////////////////////////////////////////////////////////////////////////////////////
public void setType(Type type) {
this.type = type;
typeProperty.set(type);
applyPriceToConsumer();
}
public void setCurrencyCode(String currencyCode) {
if (this.currencyCode == null || !this.currencyCode.equals(currencyCode)) {
this.currencyCode = currencyCode;
currencyCodeProperty.set(currencyCode);
applyPriceToConsumer();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
///////////////////////////////////////////////////////////////////////////////////////////
public Type getType() {
return type;
}
public String getCurrencyCode() {
return currencyCode;
}
public StringProperty currencyCodeProperty() {
return currencyCodeProperty;
}
public ObjectProperty<Type> typeProperty() {
return typeProperty;
}
public IntegerProperty currenciesUpdateFlagProperty() {
return currenciesUpdateFlag;
}
public Date getLastRequestTimeStampBtcAverage() {
return new Date(epochInSecondAtLastRequest * 1000);
}
public Date getLastRequestTimeStampPoloniex() {
Long ts = timeStampMap.get("btcAverageTs");
if (ts != null) {
Date date = new Date(ts * 1000);
return date;
} else
return new Date();
}
public Date getLastRequestTimeStampCoinmarketcap() {
Long ts = timeStampMap.get("coinmarketcapTs");
if (ts != null) {
Date date = new Date(ts * 1000);
return date;
} else
return new Date();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void applyPriceToConsumer() {
if (priceConsumer != null && currencyCode != null && type != null) {
if (cache.containsKey(currencyCode)) {
try {
MarketPrice marketPrice = cache.get(currencyCode);
priceConsumer.accept(marketPrice.getPrice(type));
} catch (Throwable t) {
log.warn("Error at applyPriceToConsumer " + t.getMessage());
}
} else {
String errorMessage = "We don't have a price for " + currencyCode;
log.debug(errorMessage);
faultHandler.handleFault(errorMessage, new PriceRequestException(errorMessage));
}
}
currenciesUpdateFlag.setValue(currenciesUpdateFlag.get() + 1);
}
private void requestAllPrices(PriceProvider provider, Runnable resultHandler, FaultHandler faultHandler) {
Log.traceCall();
PriceRequest priceRequest = new PriceRequest();
SettableFuture<Tuple2<Map<String, Long>, Map<String, MarketPrice>>> future = priceRequest.requestAllPrices(provider);
Futures.addCallback(future, new FutureCallback<Tuple2<Map<String, Long>, Map<String, MarketPrice>>>() {
@Override
public void onSuccess(@Nullable Tuple2<Map<String, Long>, Map<String, MarketPrice>> result) {
UserThread.execute(() -> {
checkNotNull(result, "Result must not be null at requestAllPrices");
timeStampMap = result.first;
epochInSecondAtLastRequest = timeStampMap.get("btcAverageTs");
cache.putAll(result.second);
resultHandler.run();
});
}
@Override
public void onFailure(Throwable throwable) {
UserThread.execute(() -> faultHandler.handleFault("Could not load marketPrices", throwable));
}
});
}
}