All Downloads are FREE. Search and download functionalities are using the official Maven repository.

estonlabs.cxtl.exchanges.mexc.spot.v3.MEXCCex Maven / Gradle / Ivy

There is a newer version: 1.4.14
Show newest version
package estonlabs.cxtl.exchanges.mexc.spot.v3;

import estonlabs.cxtl.common.auth.Credentials;
import estonlabs.cxtl.common.exception.CxtlApiException;
import estonlabs.cxtl.common.exception.CxtlEventException;
import estonlabs.cxtl.common.exception.ErrorCode;
import estonlabs.cxtl.common.http.Event;
import estonlabs.cxtl.common.http.JsonRestClient;
import estonlabs.cxtl.common.http.Method;
import estonlabs.cxtl.common.http.MetricsLogger;
import estonlabs.cxtl.exchanges.a.specification.domain.AssetClass;
import estonlabs.cxtl.exchanges.a.specification.domain.Exchange;
import estonlabs.cxtl.exchanges.a.specification.lib.Cex;
import estonlabs.cxtl.exchanges.a.specification.lib.ExchangeDataInterface;
import estonlabs.cxtl.exchanges.mexc.spot.v3.domain.*;
import lombok.NonNull;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;

public class MEXCCex implements Cex, ExchangeDataInterface {

    private static final String API_KEY_HEADER = "X-MEXC-APIKEY";

    private static final MediaType FORM_MEDIA_TYPE = MediaType.parse("application/x-www-form-urlencoded");
    private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");

    private final JsonRestClient client;
    private final MetricsLogger metricsLogger;

    public MEXCCex(JsonRestClient restClient, MetricsLogger metricsLogger) {
        this.client = restClient;
        this.metricsLogger = metricsLogger;
    }

    public Mono getOrderBook(String symbol, int limit) {
        return handleResponse(client.get("/depth", new OrderBookRequest().setSymbol(symbol).setLimit(limit), OrderBookSnapshot.class));
    }

    @Override
    public Mono> getLatestPublicTrades(AssetClass assetClass, String symbol) {
        return handleResponse(client.getMany("/trades", new TradeRequest().setSymbol(symbol), MEXCTrade.class));
    }

    @Override
    public Mono>> getTickers() {
        return handleResponse(client.get("/exchangeInfo", ExchangeInfo.class)).map(r -> Map.of(AssetClass.SPOT, r.getSymbols()));
    }

    @Override
    public Mono> getOlhcv(KlineRequest klineRequest) {
        return handleResponse(client.getMany("/klines", klineRequest, Kline.class));
    }

    @Override
    public Exchange getExchange() {
        return Exchange.MEXC;
    }

    @Override
    public AssetClass[] getSupportedAssetClasses() {
        return new AssetClass[]{AssetClass.SPOT};
    }

    private Map accessKeyHeader(Credentials credentials){
        return Map.of(API_KEY_HEADER, credentials.getApiKey());
    }

    @Override
    public Mono placeOrder(Credentials credentials, NewOrderRequest order) {
        return handleResponse(
                client.parameterisedRequest(
                        Method.POST,
                        accessKeyHeader(credentials),
                        "/order",
                        null,
                        RequestBody.create(order.queryString(credentials), JSON_MEDIA_TYPE),
                        NewOrderResponse.class
                ));
    }

    @Override
    public Mono cancelOrder(Credentials credentials, CancelRequest request) {
        return handleResponse(
                client.parameterisedRequest(
                        Method.DELETE,
                        accessKeyHeader(credentials),
                        "/order",
                        request.queryString(credentials),
                        RequestBody.create("", JSON_MEDIA_TYPE),
                        CancelResponse.class
                ));
    }

    @Override
    public Mono> getOrders(Credentials credentials, OrderQueryRequest request) {
        return handleResponse(
                client.parameterisedRequestMany(
                        Method.GET,
                        accessKeyHeader(credentials),
                        "/order",
                        request.queryString(credentials),
                        null,
                        OrderQueryResponse.class
                ));
    }

    @Override
    public Mono getOrder(Credentials credentials, OrderQueryRequest request) {
        return handleResponse(client.parameterisedRequest(
                Method.GET,
                accessKeyHeader(credentials),
                "/order",
                request.queryString(credentials),
                null,
                OrderQueryResponse.class
        ));
    }

    public Mono createListenKey(Credentials credentials, long timestamp) {
        var request = new WSListenKeyRequest().setTimestamp(timestamp);
        return handleResponse(
                client.parameterisedRequest(
                        Method.POST,
                        accessKeyHeader(credentials),
                        "/userDataStream",
                        null,
                        RequestBody.create(request.queryString(credentials), JSON_MEDIA_TYPE),
                        WSListenKey.class)
        );
    }

    public Mono refreshListenKey(Credentials credentials, WSListenKeyRequest listenKey) {
        return handleResponse(
                client.parameterisedRequest(
                        Method.PUT,
                        accessKeyHeader(credentials),
                        "/userDataStream",
                        null,
                        RequestBody.create(listenKey.queryString(credentials), JSON_MEDIA_TYPE),
                        WSListenKey.class
                ));
    }

    public Mono> getOpenOrders(Credentials credentials, long timestamp, String symbol) {
        var request = new OrderQueryRequest()
                .setTimestamp(timestamp)
                .setSymbol(symbol);
        return handleResponse(client.parameterisedRequestMany(
                Method.GET,
                accessKeyHeader(credentials),
                 "/openOrders",
                request.queryString(credentials),
                null,
                OrderQueryResponse.class
        ));
    }

    private  I onSuccess(Event e) {
        metricsLogger.finishedSuccess(e);
        return e.getResponse();
    }

    @NonNull
    private  Mono onError(Throwable t) {
        if(t instanceof CxtlEventException e){
            metricsLogger.finishedError(e.getEvent());
            ErrorResponse response = client.getCodec().quietFromJson(e.getEvent().getResponseJson(), ErrorResponse.class);
            if (response != null){
                return Mono.error(new CxtlApiException(response.toString(), response.getMsg(), mapErrorCode(response.getCode(), response.getMsg())));
            }
        }
        return Mono.error(new CxtlApiException(t.getMessage(), "UNKNOWN", ErrorCode.UNKNOWN_ERROR, t));
    }

    private ErrorCode mapErrorCode(int mexcErrorCode, String msg) {
        // if price is invalid, they send errCode=700004
        if (mexcErrorCode == 400 && msg.equals("price and quantity must be positive")){
            return ErrorCode.INVALID_QTY;
        }
        return switch (mexcErrorCode) {
            case 10101, 30004 -> ErrorCode.INSUFFICIENT_BALANCE;
            case 10095, 10096, 10097, 10102, 30032, 30029 -> ErrorCode.INVALID_QTY;
            case 30010, 30026, 700004 -> ErrorCode.BAD_PX;
            case 10007, 30014, 30016, 30021 -> ErrorCode.INVALID_SYMBOL;
            case -2011 -> ErrorCode.UNKNOWN_ORDER;
            default -> ErrorCode.UNKNOWN_ERROR;
        };
    }

    private  Mono handleResponse(Mono> response){
        return response.map(this::onSuccess).onErrorResume(this::onError);
    }

}