info.bitrich.xchangestream.okex.OkexStreamingService Maven / Gradle / Ivy
The newest version!
package info.bitrich.xchangestream.okex;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import info.bitrich.xchangestream.okex.dto.OkexLoginMessage;
import info.bitrich.xchangestream.okex.dto.OkexSubscribeMessage;
import info.bitrich.xchangestream.service.netty.JsonNettyStreamingService;
import info.bitrich.xchangestream.service.netty.WebSocketClientHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.CompletableSource;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.knowm.xchange.ExchangeSpecification;
import org.knowm.xchange.exceptions.ExchangeException;
import org.knowm.xchange.exceptions.NotYetImplementedForExchangeException;
import org.knowm.xchange.okex.dto.OkexInstType;
import org.knowm.xchange.service.BaseParamsDigest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OkexStreamingService extends JsonNettyStreamingService {
private static final Logger LOG = LoggerFactory.getLogger(OkexStreamingService.class);
private static final String LOGIN_SIGN_METHOD = "GET";
private static final String LOGIN_SIGN_REQUEST_PATH = "/users/self/verify";
private static final String SUBSCRIBE = "subscribe";
private static final String UNSUBSCRIBE = "unsubscribe";
public static final String TRADES = "trades";
public static final String ORDERBOOK = "books";
public static final String ORDERBOOK5 = "books5";
public static final String FUNDING_RATE = "funding-rate";
public static final String TICKERS = "tickers";
public static final String USERTRADES = "orders";
private final Observable pingPongSrc = Observable.interval(15, 15, TimeUnit.SECONDS);
private WebSocketClientHandler.WebSocketMessageHandler channelInactiveHandler = null;
private Disposable pingPongSubscription;
private final ExchangeSpecification xSpec;
private volatile boolean loginDone = false;
private volatile boolean needToResubscribeChannels = false;
public OkexStreamingService(String apiUrl, ExchangeSpecification exchangeSpecification) {
super(apiUrl);
this.xSpec = exchangeSpecification;
}
@Override
public Completable connect() {
loginDone = xSpec.getApiKey() == null;
Completable conn = super.connect();
return conn.andThen(
(CompletableSource)
(completable) -> {
try {
if (xSpec.getApiKey() != null) {
login();
}
if (pingPongSubscription != null && !pingPongSubscription.isDisposed()) {
pingPongSubscription.dispose();
}
pingPongSubscription = pingPongSrc.subscribe(o -> this.sendMessage("ping"));
completable.onComplete();
} catch (Exception e) {
completable.onError(e);
}
});
}
@Override
public void resubscribeChannels() {
needToResubscribeChannels = true;
if (loginDone) {
super.resubscribeChannels();
}
}
public void login() throws JsonProcessingException {
Mac mac;
try {
mac = Mac.getInstance(BaseParamsDigest.HMAC_SHA_256);
final SecretKey secretKey =
new SecretKeySpec(
xSpec.getSecretKey().getBytes(StandardCharsets.UTF_8), BaseParamsDigest.HMAC_SHA_256);
mac.init(secretKey);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new ExchangeException("Invalid API secret", e);
}
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String toSign = timestamp + LOGIN_SIGN_METHOD + LOGIN_SIGN_REQUEST_PATH;
String sign =
Base64.getEncoder().encodeToString(mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)));
OkexLoginMessage message = new OkexLoginMessage();
String passphrase = xSpec.getExchangeSpecificParametersItem("passphrase").toString();
OkexLoginMessage.LoginArg loginArg =
new OkexLoginMessage.LoginArg(xSpec.getApiKey(), passphrase, timestamp, sign);
message.getArgs().add(loginArg);
this.sendMessage(objectMapper.writeValueAsString(message));
}
@Override
public void messageHandler(String message) {
LOG.debug("Received message: {}", message);
JsonNode jsonNode;
// Parse incoming message to JSON
try {
jsonNode = objectMapper.readTree(message);
} catch (IOException e) {
if ("pong".equals(message)) {
// ping pong message
return;
}
LOG.error("Error parsing incoming message to JSON: {}", message);
return;
}
// Retry after a successful login
if (jsonNode.has("event")) {
String event = jsonNode.get("event").asText();
if ("login".equals(event)) {
loginDone = true;
if (needToResubscribeChannels) {
this.resubscribeChannels();
needToResubscribeChannels = false;
}
return;
}
}
if (processArrayMessageSeparately() && jsonNode.isArray()) {
// In case of array - handle every message separately.
for (JsonNode node : jsonNode) {
handleMessage(node);
}
} else {
handleMessage(jsonNode);
}
}
@Override
protected String getChannelNameFromMessage(JsonNode message) {
String channelName = "";
if (message.has("arg")) {
if (message.get("arg").has("channel") && message.get("arg").has("instId")) {
channelName =
message.get("arg").get("channel").asText() + message.get("arg").get("instId").asText();
}
}
return channelName;
}
@Override
public String getSubscribeMessage(String channelName, Object... args) throws IOException {
return objectMapper.writeValueAsString(
new OkexSubscribeMessage(SUBSCRIBE, Collections.singletonList(getTopic(channelName))));
}
@Override
public String getUnsubscribeMessage(String channelName, Object... args) throws IOException {
return objectMapper.writeValueAsString(
new OkexSubscribeMessage(UNSUBSCRIBE, Collections.singletonList(getTopic(channelName))));
}
private OkexSubscribeMessage.SubscriptionTopic getTopic(String channelName) {
if (channelName.contains(ORDERBOOK5)) {
return new OkexSubscribeMessage.SubscriptionTopic(
ORDERBOOK5, null, null, channelName.replace(ORDERBOOK5, ""));
} else if (channelName.contains(ORDERBOOK)) {
return new OkexSubscribeMessage.SubscriptionTopic(
ORDERBOOK, null, null, channelName.replace(ORDERBOOK, ""));
} else if (channelName.contains(TRADES)) {
return new OkexSubscribeMessage.SubscriptionTopic(
TRADES, null, null, channelName.replace(TRADES, ""));
} else if (channelName.contains(TICKERS)) {
return new OkexSubscribeMessage.SubscriptionTopic(
TICKERS, null, null, channelName.replace(TICKERS, ""));
} else if (channelName.contains(USERTRADES)) {
return new OkexSubscribeMessage.SubscriptionTopic(
USERTRADES, OkexInstType.ANY, null, channelName.replace(USERTRADES, ""));
} else if (channelName.contains(FUNDING_RATE)) {
return new OkexSubscribeMessage.SubscriptionTopic(
FUNDING_RATE, null, null, channelName.replace(FUNDING_RATE, ""));
} else {
throw new NotYetImplementedForExchangeException(
"ChannelName: "
+ channelName
+ " has not implemented yet on "
+ this.getClass().getSimpleName());
}
}
@Override
protected WebSocketClientHandler getWebSocketClientHandler(
WebSocketClientHandshaker handshake, WebSocketClientHandler.WebSocketMessageHandler handler) {
LOG.info("Registering OkxWebSocketClientHandler");
return new OkxWebSocketClientHandler(handshake, handler);
}
public void setChannelInactiveHandler(
WebSocketClientHandler.WebSocketMessageHandler channelInactiveHandler) {
this.channelInactiveHandler = channelInactiveHandler;
}
/**
* Custom client handler in order to execute an external, user-provided handler on channel events.
*/
class OkxWebSocketClientHandler extends NettyWebSocketClientHandler {
public OkxWebSocketClientHandler(
WebSocketClientHandshaker handshake, WebSocketMessageHandler handler) {
super(handshake, handler);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
super.channelInactive(ctx);
if (channelInactiveHandler != null) {
channelInactiveHandler.onMessage("WebSocket Client disconnected!");
}
}
}
public void pingPongDisconnectIfConnected() {
if (pingPongSubscription != null && !pingPongSubscription.isDisposed()) {
pingPongSubscription.dispose();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy