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

estonlabs.cxtl.exchanges.hyperliquid.api.v0.lib.HyperliquidRestClient Maven / Gradle / Ivy

There is a newer version: 1.4.14
Show newest version
package estonlabs.cxtl.exchanges.hyperliquid.api.v0.lib;

import java.math.BigDecimal;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import estonlabs.cxtl.exchanges.a.specification.domain.*;
import estonlabs.cxtl.exchanges.coinbase.api.v3.domain.request.CandleRequest;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import estonlabs.cxtl.common.auth.Credentials;
import estonlabs.cxtl.common.exception.CxtlApiException;
import estonlabs.cxtl.common.exception.ErrorCode;
import estonlabs.cxtl.common.http.Event;
import estonlabs.cxtl.common.http.JsonRestClient;
import estonlabs.cxtl.common.http.MetricsLogger;
import estonlabs.cxtl.common.http.RestClient;
import estonlabs.cxtl.exchanges.a.specification.lib.Cex;
import estonlabs.cxtl.exchanges.a.specification.lib.ExchangeDataInterface;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.CancelOrderAction;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.CancelRequest;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.CreateOrderAction;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.CreateOrderAction.HyperliquidOrder;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.CreateOrderAction.Limit;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.CreateOrderAction.OrderType;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.ExchApiRequest;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.InfoApiRequest;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.OrderQueryRequest;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.request.OrderRequest;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.ApiResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.CancelAck;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.CancelAckResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.CreateAck;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.CreateAckResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.ExchangeApiResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.HyperliquidFilledOrder;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.HyperliquidFilledOrderList;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.HyperliquidOpenOrder;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.HyperliquidOpenOrderList;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.HyperliquidTicker;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.ListResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.OrderResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.TickerListResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.response.TradeListResponse;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.types.OrderStatus;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.domain.types.RequestType;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.util.HyperliquidSignature;
import estonlabs.cxtl.exchanges.hyperliquid.api.v0.util.HyperliquidSigner;
import reactor.core.publisher.Mono;


public class HyperliquidRestClient implements Cex, ExchangeDataInterface {
    private static final Logger LOGGER = LoggerFactory.getLogger(HyperliquidRestClient.class);
    private static final String INFO_ENDPOINT_URL = "/info";
    private static final String EXCHANGE_ENDPOINT_URL = "/exchange";

    //These are acceptable order size range by hyperliquid API. 
    //"Failed to deserialize the JSON body into the target type" error will be thrown if out of these range, so need to valid the order size manually to make sure the order can be processed by hyperliquid API
    private static final BigDecimal SYS_MIN_ORDER_SIZE = new BigDecimal("0.00000001");
    private static final BigDecimal SYS_MAX_ORDER_SIZE = new BigDecimal("99999999999");
    
    private final RestClientAdapter client;

    public HyperliquidRestClient(JsonRestClient restClient, URI baseUri, MetricsLogger metricsLogger) {
        this.client = new RestClientAdapter(restClient, baseUri, metricsLogger);
    }

    @Override
    public Exchange getExchange() {
        return Exchange.HYPERLIQUID;
    }
    
    @Override
    public AssetClass[] getSupportedAssetClasses() {
        return new AssetClass[] { AssetClass.PERP };
    }

    @Override
    public Mono> getOlhcv(Object request) {
        return Mono.empty();
    }

    @NotNull
    @Override
    public Mono placeOrder(@NotNull Credentials credentials, @NotNull OrderRequest order) {
        
        
        if(order.getSize().doubleValue() == 0) {
            return Mono.error(new CxtlApiException("Order has zero size", null, ErrorCode.INVALID_QTY));
        }
        
        if(order.getSize().compareTo(HyperliquidRestClient.SYS_MAX_ORDER_SIZE)> 0) {
            return Mono.error(new CxtlApiException("Order size too big", null, ErrorCode.INVALID_QTY));
        }
        if(order.getSize().compareTo(SYS_MIN_ORDER_SIZE) < 0) {
            return Mono.error(new CxtlApiException("Order size too small", null, ErrorCode.INVALID_QTY));
        }
        
        Mono mono = createOrder(order);
        CreateOrderAction action = mono.block();
        
        try {
            long nonce = System.currentTimeMillis();
            HyperliquidSignature signature = HyperliquidSigner.signL1Action(credentials.getSecretKey(), action, null, System.currentTimeMillis(), true);
            ExchApiRequest apiRequest = new ExchApiRequest<>(action, nonce, signature);
            
            return client.postExchange(apiRequest, CreateAckResponse.class);
        } catch (Exception e) {
            return Mono.error(new CxtlApiException("Unable to create order", "SYMBOL_NOT_FOUND", ErrorCode.UNKNOWN_ERROR));
        }
        
    }

    @NotNull
    @Override
    public Mono cancelOrder(@NotNull Credentials credentials, @NotNull CancelRequest request) {
        
        Mono mono = cancelOrder(request);
        CancelOrderAction action = mono.block();
        
        try {
            long nonce = System.currentTimeMillis();
            HyperliquidSignature signature = HyperliquidSigner.signL1Action(credentials.getSecretKey(), action, null, System.currentTimeMillis(), true);
            ExchApiRequest apiRequest = new ExchApiRequest<>(action, nonce, signature);

            return client.postExchange(apiRequest, CancelAckResponse.class);
        } catch (Exception e) {
            return Mono.error(new CxtlApiException("Unable to cancel order", "SYMBOL_NOT_FOUND", ErrorCode.UNKNOWN_ERROR));
        }
    }

    @NotNull
    @Override
    public Mono> getOrders(@NotNull Credentials credentials, OrderQueryRequest orderQueryRequest) {
        Mono> openOrders  = Mono.just(List.of());
        Mono> fillOrders = Mono.just(List.of());
        
        if(orderQueryRequest.getStatus() == OrderStatus.open || orderQueryRequest.getStatus() == null) {
            InfoApiRequest request = InfoApiRequest.builder()
                    .type(RequestType.openOrders)
                    .user(credentials.getApiKey())
                    .build();
            
            openOrders =  client.postInfoForList(request, HyperliquidOpenOrderList.class);
        }
        
        if(orderQueryRequest.getStatus() == OrderStatus.filled || orderQueryRequest.getStatus() == null) {
            InfoApiRequest request = InfoApiRequest.builder()
                    .type(RequestType.userFills)
                    .user(credentials.getApiKey())
                    .build();
            
            fillOrders =  client.postInfoForList(request, HyperliquidFilledOrderList.class);
        }
        return Mono.zip(openOrders, fillOrders)
                .flatMap(tuple -> {
                    List combinedList = new ArrayList<>();
                    combinedList.addAll(tuple.getT1());
                    combinedList.addAll(tuple.getT2());
                    return Mono.just(combinedList);
                });
    }

    @Override
    public Mono getOrder(Credentials credentials, OrderQueryRequest orderQueryRequest) {
        InfoApiRequest request = InfoApiRequest.builder()
                .type(RequestType.orderStatus)
                .user(credentials.getApiKey())
                .oid(orderQueryRequest.getOid() != null ? Long.valueOf(orderQueryRequest.getOid()): orderQueryRequest.getClOid())
                .build();
                
        return client.postInfo(request, OrderResponse.class);
    }
    
    @Override
    public Mono>> getTickers() {
        return getTickers(AssetClass.PERP)
                .map(tickers -> {
                    Map> tickerMap = new HashMap<>();
                    tickerMap.put(AssetClass.PERP, tickers);
                    return tickerMap;
                });
    }

  
        
    public Mono> getTickers(AssetClass assetClass) {
        if(Arrays.stream(getSupportedAssetClasses()).noneMatch(ac -> ac == assetClass)){
            return Mono.error(new CxtlApiException("Asset class not supported", "ASSET_CLASS_NOT_SUPPORTED", ErrorCode.INVALID_SYMBOL));
        }

        InfoApiRequest req = new InfoApiRequest();
        
        if(assetClass == AssetClass.PERP) {
            req.setType(RequestType.meta);
        }

        return client.postInfo(req, TickerListResponse.class)
                .switchIfEmpty(Mono.just(List.of()));
    }
    
    @Override
    public Mono> getLatestPublicTrades(AssetClass assetClass, String symbol) {
        InfoApiRequest request = InfoApiRequest.builder().type(RequestType.allMids).build();
        return client.postInfo(request, TradeListResponse.class)
                .flatMapIterable(trades -> trades) 
                .filter(trade -> trade.getSymbol().equals(symbol)) 
                .collectList()
                .flatMap(filteredTrades -> {
                    if (filteredTrades.isEmpty()) {
                        return Mono.error(new CxtlApiException("Symbol not found in tickers", "SYMBOL_NOT_FOUND", ErrorCode.INVALID_SYMBOL));
                    } else {
                        return Mono.just(filteredTrades);
                    }
                });
    }
    
    private Mono createOrder(OrderRequest request) {
       
        String symbol = request.getSymbol();
        return this.getTickers(AssetClass.PERP).flatMap(tickers -> {
            // Find the index of the symbol in the tickers list
            int index = -1;
            for (int i = 0; i < tickers.size(); i++) {
                if (tickers.get(i).getSymbol().equals(symbol)) {
                    index = i;
                    break;
                }
            }

            if (index == -1)
                return Mono.error(new CxtlApiException("Symbol not found in tickers", "SYMBOL_NOT_FOUND", ErrorCode.INVALID_SYMBOL));

            OrderType type = new OrderType();
            type.setLimit(new Limit(request.getTif()));

            HyperliquidOrder order = HyperliquidOrder.builder()
                    .assetId(index)  // Use the index as the assetId
                    .buy(request.isBuy())
                    .price(request.getPrice().toPlainString())
                    .reduceOnly(false)
                    .orderType(type)
                    .size(request.getSize().stripTrailingZeros().toPlainString())
                    .clientOrderId(request.getCloid())
                    .build();

            return Mono.just(CreateOrderAction.createOrder(order));
        });
    }


    private Mono cancelOrder(CancelRequest request) {
        String symbol = request.getSymbol();
        return this.getTickers(request.getAssetClass()).flatMap(tickers -> {
                // Find the index of the symbol in the tickers list
            int index = -1;
            for (int i = 0; i < tickers.size(); i++) {
                if (tickers.get(i).getSymbol().equals(symbol)) {
                    index = i;
                    break;
                }
            }

            if (index == -1) {
                return Mono.error(new CxtlApiException("Symbol not found in tickers", "SYMBOL_NOT_FOUND", ErrorCode.INVALID_SYMBOL));
            }
                
            CancelOrderAction action = null;
            if(request.getCloid() != null) {
                action = CancelOrderAction.cancelByClientOrderId(index, request.getCloid());
            }else {
                action = CancelOrderAction.cancelByOrderId(index, Long.valueOf(request.getOrderId()));
            }
            return Mono.just(action);
        });
    }

    
    public static class RestClientAdapter {
        private final RestClient client;
        private final MetricsLogger metricsLogger;

        public RestClientAdapter(RestClient client, URI baseUri, MetricsLogger metricsLogger) {
            this.client = client;
            this.metricsLogger = metricsLogger;
        }

        public > Mono postInfo(IN request, Class type) {
            return client.postAsJson(INFO_ENDPOINT_URL, request, type)
                    .flatMap(this::handleInfoResponse);
        }
        
        public > Mono> postInfoForList(IN request, Class type) {
            return client.postAsJson(INFO_ENDPOINT_URL, request, type)
                    .flatMap(this::handleResponseList);
        }
        
        public > Mono postExchange(IN request, Class type) {
            return client.postAsJson(EXCHANGE_ENDPOINT_URL, request, type)
                    .flatMap(this::handleExchangeApiResponse);
        }


        private > Mono> handleResponseList(Event event) {
            List orders = event.getResponse();
            return Mono.just(orders).doFinally(v -> metricsLogger.finishedSuccess(event));
        }
        
        private > Mono handleExchangeApiResponse(Event event) {
            LOGGER.info("event resp {}", event.getResponseJson());
            ExchangeApiResponse output = event.getResponse();
            if(!output.isSuccess()) {
                metricsLogger.finishedError(event);
                String errorMessage = output.getErrorMessage();
                return Mono.error(new CxtlApiException(errorMessage, null, errorCode(errorMessage)));
            }
            return Mono.just(event.getResponse().getAck()).doFinally(v -> metricsLogger.finishedSuccess(event));
        }

        private > Mono handleInfoResponse(Event event) {
            ApiResponse response = event.getResponse();
            if(!response.isSuccess()) {
                metricsLogger.finishedError(event);
                return Mono.error(
                        new CxtlApiException(response.getErrorMessage(), null, null));
            }
            return Mono.just(event.getResponse().getResult()).doFinally(v -> metricsLogger.finishedSuccess(event));
        }
    }

    private static ErrorCode errorCode(String message) {
        if(message.contains("Order must have minimum value of $10")||
                message.contains("Order has invalid size")||
                message.contains("Order value too large. Max is $10000000.")) {
            return ErrorCode.INVALID_QTY;
        }else if(message.contains("Insufficient margin to place order")){
            return ErrorCode.INSUFFICIENT_BALANCE;
        }else if(message.contains("Price must be divisible by tick size")||
                message.contains("Invalid TP/SL price")) {
            return ErrorCode.BAD_PX;
        }else if(message.contains("Order was never placed, already canceled, or filled")||
                message.contains("Order could not immediately match against any resting orders.")){
            return ErrorCode.UNKNOWN_ORDER;
        }        
        return ErrorCode.UNKNOWN_ERROR;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy