diff --git a/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApi.cs b/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApi.cs index accc88d..1bfba91 100644 --- a/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApi.cs +++ b/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApi.cs @@ -16,6 +16,7 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.Converters.MessageParsing; +using HyperLiquid.Net.Objects.Models; namespace HyperLiquid.Net.Clients.Api { @@ -80,6 +81,16 @@ internal async Task> SendToAddressAsync(string baseAddress, return result; } + internal async Task> SendAuthAsync(RequestDefinition definition, ParameterCollection? parameters, CancellationToken cancellationToken, int? weight = null) + { + var result = await SendToAddressAsync>(BaseAddress, definition, parameters, cancellationToken, weight).ConfigureAwait(false); + if (!result) + return result.As(default); + + return result.As(result.Data.Data); + } + + protected override Error? TryParseError(IEnumerable>> responseHeaders, IMessageAccessor accessor) { var status = accessor.GetValue(MessagePath.Get().Property("status")); diff --git a/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApiTrading.cs b/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApiTrading.cs index fe9cb5d..290c7b5 100644 --- a/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApiTrading.cs +++ b/HyperLiquid.Net/Clients/Api/HyperLiquidRestClientApiTrading.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using System.Threading; using System; +using HyperLiquid.Net.Utils; +using HyperLiquid.Net.Enums; namespace HyperLiquid.Net.Clients.Api { @@ -140,12 +142,94 @@ public async Task>> GetClosedO #endregion + #region Place Multiple Orders + + /// + public async Task>>> PlaceMultipleOrdersAsync( + IEnumerable orders, + CancellationToken ct = default) + { + var orderRequests = new List(); + foreach(var order in orders) + { + var symbolId = await HyperLiquidUtils.GetSymbolIdFromName(order.SymbolType, order.Symbol).ConfigureAwait(false); + if (!symbolId) + return new WebCallResult>>(symbolId.Error); + + var orderParameters = new ParameterCollection(); + orderParameters.Add("a", symbolId.Data); + orderParameters.AddString("s", order.Quantity); + + var orderTypeParameters = new ParameterCollection(); + if (order.OrderType == OrderType.Limit) + { + var limitParameters = new ParameterCollection(); + limitParameters.AddEnum("tif", order.TimeInForce); + orderTypeParameters.Add("limit", limitParameters); + } + else if(order.OrderType == OrderType.StopMarket || order.OrderType == OrderType.StopLimit) + { + var triggerParameters = new ParameterCollection(); + triggerParameters.Add("isMarket", order.TimeInForce); + triggerParameters.Add("triggerPx", order.TriggerPrice); + triggerParameters.AddEnum("tpsl", order.TpSlType); + orderTypeParameters.Add("trigger", triggerParameters); + } + + orderParameters.Add("t", orderTypeParameters); + + orderParameters.AddOptional("b", order.Side == OrderSide.Buy); + orderParameters.AddOptionalString("p", order.Price); + orderParameters.AddOptional("r", order.ReduceOnly); + orderParameters.AddOptional("c", order.ClientOrderId); +#warning TODO builder + if (order.TpSlGrouping != null) + orderParameters.AddEnum("grouping", order.TpSlGrouping); + else + orderParameters.Add("grouping", "na"); + + orderRequests.Add(orderParameters); + } + + var parameters = new ParameterCollection() + { + { + "action", new ParameterCollection + { + { "type", "order" }, + { "orders", orderRequests } + } + } + }; + + var request = _definitions.GetOrCreate(HttpMethod.Post, "exchange", HyperLiquidExchange.RateLimiter.HyperLiquid, 1, true); + var intResult = await _baseClient.SendAuthAsync>(request, parameters, ct).ConfigureAwait(false); + if(!intResult) + return intResult.As>>(default); + + var result = new List>(); + foreach (var order in intResult.Data) + { + if (order.Error != null) + result.Add(new CallResult(new ServerError(order.Error))); + else + result.Add(new CallResult(order.ResultResting ?? order.ResultFilled!)); + } + + return intResult.As>>(result); + } + + #endregion + #region Cancel Order /// - public async Task>> CancelOrderAsync(string symbol, long orderId, CancellationToken ct = default) + public async Task CancelOrderAsync(SymbolType symbolType, string symbol, long orderId, CancellationToken ct = default) { - var symbolId = 1; // TODO + var symbolId = await HyperLiquidUtils.GetSymbolIdFromName(symbolType, symbol).ConfigureAwait(false); + if (!symbolId) + return new WebCallResult(symbolId.Error!); + var parameters = new ParameterCollection() { { @@ -157,7 +241,7 @@ public async Task>> CancelOrde { new ParameterCollection { - { "a", symbolId }, + { "a", symbolId.Data }, { "o", orderId } } } @@ -167,7 +251,13 @@ public async Task>> CancelOrde }; var request = _definitions.GetOrCreate(HttpMethod.Post, "exchange", HyperLiquidExchange.RateLimiter.HyperLiquid, 1, true); - return await _baseClient.SendAsync>(request, parameters, ct).ConfigureAwait(false); + var result = await _baseClient.SendAuthAsync(request, parameters, ct).ConfigureAwait(false); + if (!result) + return result.AsDatalessError(result.Error!); + +#warning check responses + + return result.AsDataless(); } #endregion diff --git a/HyperLiquid.Net/Enums/SymbolType.cs b/HyperLiquid.Net/Enums/SymbolType.cs new file mode 100644 index 0000000..e931e85 --- /dev/null +++ b/HyperLiquid.Net/Enums/SymbolType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HyperLiquid.Net.Enums +{ + public enum SymbolType + { + Spot, + Futures + } +} diff --git a/HyperLiquid.Net/Enums/TimeInForce.cs b/HyperLiquid.Net/Enums/TimeInForce.cs new file mode 100644 index 0000000..757c5d6 --- /dev/null +++ b/HyperLiquid.Net/Enums/TimeInForce.cs @@ -0,0 +1,29 @@ +using CryptoExchange.Net.Attributes; +using System; +using System.Collections.Generic; +using System.Text; + +namespace HyperLiquid.Net.Enums +{ + /// + /// Time in force + /// + public enum TimeInForce + { + /// + /// Post only + /// + [Map("Alo")] + PostOnly, + /// + /// Immediate or cancel + /// + [Map("Ioc")] + ImmediateOrCancel, + /// + /// Good till canceled + /// + [Map("Gtc")] + GoodTillCanceled + } +} diff --git a/HyperLiquid.Net/Enums/TpSlGrouping.cs b/HyperLiquid.Net/Enums/TpSlGrouping.cs new file mode 100644 index 0000000..1a03077 --- /dev/null +++ b/HyperLiquid.Net/Enums/TpSlGrouping.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HyperLiquid.Net.Enums +{ + /// + /// TakeProfit/StopLoss grouping + /// + public enum TpSlGrouping + { + /// + /// Normal TakeProfit/StopLoss + /// + NormalTpSl, + /// + /// Position TakeProfit/StopLoss + /// + PositionTpSl + } +} diff --git a/HyperLiquid.Net/Enums/TpSlType.cs b/HyperLiquid.Net/Enums/TpSlType.cs new file mode 100644 index 0000000..3bcb852 --- /dev/null +++ b/HyperLiquid.Net/Enums/TpSlType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HyperLiquid.Net.Enums +{ + public enum TpSlType + { + TakeProfit, + StopLoss + } +} diff --git a/HyperLiquid.Net/HyperLiquid.Net.xml b/HyperLiquid.Net/HyperLiquid.Net.xml index e22b9b7..4d46f67 100644 --- a/HyperLiquid.Net/HyperLiquid.Net.xml +++ b/HyperLiquid.Net/HyperLiquid.Net.xml @@ -97,7 +97,10 @@ - + + + + @@ -287,6 +290,41 @@ Stop Limit + + + Time in force + + + + + Post only + + + + + Immediate or cancel + + + + + Good till canceled + + + + + TakeProfit/StopLoss grouping + + + + + Normal TakeProfit/StopLoss + + + + + Position TakeProfit/StopLoss + + Extension methods specific to using the HyperLiquid API @@ -903,21 +941,6 @@ - - - Message pack converter - - - - - Convert an object to byte array - - - - - Convert an object to byte array - - HyperLiquid order book factory @@ -973,6 +996,21 @@ + + + Message pack converter + + + + + Convert an object to byte array + + + + + Convert an object to byte array + + Extensions for the ICryptoRestClient and ICryptoSocketClient interfaces diff --git a/HyperLiquid.Net/HyperLiquidAuthenticationProvider.cs b/HyperLiquid.Net/HyperLiquidAuthenticationProvider.cs index 1e9b213..06ca97a 100644 --- a/HyperLiquid.Net/HyperLiquidAuthenticationProvider.cs +++ b/HyperLiquid.Net/HyperLiquidAuthenticationProvider.cs @@ -10,6 +10,7 @@ using Nethereum.Util; using Nethereum.Signer; using Nethereum.ABI.EIP712; +using HyperLiquid.Net.Utils; namespace HyperLiquid.Net { @@ -75,15 +76,14 @@ public override void AuthenticateRequest( bodyParameters!.Add("nonce", nonce); var action = bodyParameters["action"]; - var hash = this.actionHash(action, nonce); + var hash = GenerateActionHash(action, nonce); var phantomAgent = new Dictionary() { { "source", "a" }, { "connectionId", hash }, }; - var msg = ethEncodeStructuredData(_domain, _messageTypes, phantomAgent); - + var msg = EncodeEip721(_domain, _messageTypes, phantomAgent); var keccakSigned = BytesToHexString(SignKeccak(msg)); var signature = SignRequest(keccakSigned, ApiKey); @@ -100,11 +100,11 @@ public static Dictionary SignRequest(string request, string secr { { "r", "0x" + BytesToHexString(sign.R).ToLowerInvariant() }, { "s", "0x" + BytesToHexString(sign.S).ToLowerInvariant() }, - { "v", ((int)sign.V[0]) - 27 }, + { "v", ((int)sign.V[0]) - 27 } }; } - public byte[] ethEncodeStructuredData( + public byte[] EncodeEip721( Dictionary domain, Dictionary messageTypes, Dictionary messageData) @@ -186,7 +186,7 @@ public byte[] ethEncodeStructuredData( return Eip712TypedDataSigner.Current.EncodeTypedDataRaw(typeRaw); } - private byte[] actionHash(object action, long nonce) + private byte[] GenerateActionHash(object action, long nonce) { var packer = new PackConverter(); var dataHex = BytesToHexString(packer.Pack(action)); diff --git a/HyperLiquid.Net/Interfaces/Clients/Api/IHyperLiquidRestClientApiTrading.cs b/HyperLiquid.Net/Interfaces/Clients/Api/IHyperLiquidRestClientApiTrading.cs index 713ba21..362cdcb 100644 --- a/HyperLiquid.Net/Interfaces/Clients/Api/IHyperLiquidRestClientApiTrading.cs +++ b/HyperLiquid.Net/Interfaces/Clients/Api/IHyperLiquidRestClientApiTrading.cs @@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects; using HyperLiquid.Net.Objects.Models; using System; +using HyperLiquid.Net.Enums; namespace HyperLiquid.Net.Interfaces.Clients.Api { @@ -27,6 +28,10 @@ Task>> GetUserTradesByTimeAsync( Task>> GetClosedOrdersAsync(string address, CancellationToken ct = default); - Task>> CancelOrderAsync(string symbol, long orderId, CancellationToken ct = default); + Task CancelOrderAsync(SymbolType symbolType, string symbol, long orderId, CancellationToken ct = default); + + Task>>> PlaceMultipleOrdersAsync( + IEnumerable orders, + CancellationToken ct = default); } } diff --git a/HyperLiquid.Net/Objects/Models/HyperLiquidAuthResponse.cs b/HyperLiquid.Net/Objects/Models/HyperLiquidAuthResponse.cs new file mode 100644 index 0000000..619d177 --- /dev/null +++ b/HyperLiquid.Net/Objects/Models/HyperLiquidAuthResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace HyperLiquid.Net.Objects.Models +{ + internal class HyperLiquidAuthResponse + { + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("data")] + public T Data { get; set; } + } +} diff --git a/HyperLiquid.Net/Objects/Models/HyperLiquidExchangeInfo.cs b/HyperLiquid.Net/Objects/Models/HyperLiquidExchangeInfo.cs index fa75477..fa8ffce 100644 --- a/HyperLiquid.Net/Objects/Models/HyperLiquidExchangeInfo.cs +++ b/HyperLiquid.Net/Objects/Models/HyperLiquidExchangeInfo.cs @@ -21,14 +21,18 @@ public IEnumerable Symbols { if (_symbols == null) { - _symbols = SymbolsInt.Select(x => - new HyperLiquidSymbol + _symbols = SymbolsInt.Select(x => { + var baseAsset = Assets.ElementAt(x.BaseAssetIndex); + var quoteAsset = Assets.ElementAt(x.QuoteAssetIndex); + return new HyperLiquidSymbol { Index = x.Index, IsCanonical = x.IsCanonical, - Name = x.Name, - BaseAsset = Assets.ElementAt(x.BaseAssetIndex), - QuoteAsset = Assets.ElementAt(x.QuoteAssetIndex), + Name = baseAsset.Name + "/" + quoteAsset.Name, + ExchangeName = x.Name, + BaseAsset = baseAsset, + QuoteAsset = quoteAsset, + }; } ).ToList(); } @@ -41,6 +45,7 @@ public IEnumerable Symbols public record HyperLiquidSymbol { public string Name { get; set; } + public string ExchangeName { get; set; } public HyperLiquidAsset BaseAsset { get; set; } public HyperLiquidAsset QuoteAsset { get; set; } public int Index { get; set; } diff --git a/HyperLiquid.Net/Objects/Models/HyperLiquidOrderRequest.cs b/HyperLiquid.Net/Objects/Models/HyperLiquidOrderRequest.cs new file mode 100644 index 0000000..0de79c6 --- /dev/null +++ b/HyperLiquid.Net/Objects/Models/HyperLiquidOrderRequest.cs @@ -0,0 +1,50 @@ +using HyperLiquid.Net.Enums; +using System; +using System.Collections.Generic; +using System.Text; + +namespace HyperLiquid.Net.Objects.Models +{ + public record HyperLiquidOrderRequest + { + public SymbolType SymbolType { get; set; } + public string Symbol { get; set; } + public OrderSide Side { get; set; } + public OrderType OrderType { get; set; } + public TimeInForce? TimeInForce { get; set; } + public decimal? Price { get; set; } + public decimal Quantity { get; set; } + public bool? ReduceOnly { get; set; } + public string? ClientOrderId { get; set; } + public decimal? TriggerPrice { get; set; } + public TpSlType? TpSlType { get; set; } + public TpSlGrouping? TpSlGrouping { get; set; } + + public HyperLiquidOrderRequest( + SymbolType symbolType, + string symbol, + OrderSide side, + OrderType orderType, + decimal quantity, + decimal? price = null, + TimeInForce? timeInForce = null, + bool? reduceOnly = null, + decimal? triggerPrice = null, + TpSlType? tpSlType = null, + string? clientOrderId = null + ) + { + SymbolType = symbolType; + Symbol = symbol; + Side = side; + OrderType = orderType; + Quantity = quantity; + Price = price; + ReduceOnly = reduceOnly; + TriggerPrice = triggerPrice; + TpSlType = tpSlType; + ClientOrderId = clientOrderId; + TimeInForce = timeInForce; + } + } +} diff --git a/HyperLiquid.Net/Objects/Models/HyperLiquidOrderResult.cs b/HyperLiquid.Net/Objects/Models/HyperLiquidOrderResult.cs new file mode 100644 index 0000000..e85884c --- /dev/null +++ b/HyperLiquid.Net/Objects/Models/HyperLiquidOrderResult.cs @@ -0,0 +1,29 @@ +using HyperLiquid.Net.Enums; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace HyperLiquid.Net.Objects.Models +{ + internal record HyperLiquidOrderResultInt + { + [JsonPropertyName("resting")] + public HyperLiquidOrderResult? ResultResting { get; set; } + [JsonPropertyName("filled")] + public HyperLiquidOrderResult? ResultFilled { get; set; } + [JsonPropertyName("error")] + public string? Error { get; set; } + } + + public record HyperLiquidOrderResult + { + [JsonPropertyName("oid")] + public long OrderId { get; set; } + public OrderStatus Status { get; set; } + [JsonPropertyName("totalSz")] + public decimal? FilledQuantity { get; set; } + [JsonPropertyName("avgPx")] + public decimal? AveragePrice { get; set; } + } +} diff --git a/HyperLiquid.Net/Utils/HyperLiquidUtils.cs b/HyperLiquid.Net/Utils/HyperLiquidUtils.cs new file mode 100644 index 0000000..284ccf6 --- /dev/null +++ b/HyperLiquid.Net/Utils/HyperLiquidUtils.cs @@ -0,0 +1,88 @@ +using CryptoExchange.Net.Objects; +using HyperLiquid.Net.Clients; +using HyperLiquid.Net.Enums; +using HyperLiquid.Net.Objects.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace HyperLiquid.Net.Utils +{ + public static class HyperLiquidUtils + { + private static IEnumerable? _spotSymbolInfo; + private static IEnumerable? _futuresSymbolInfo; + + private static DateTime _lastUpdateTime; + + private static readonly SemaphoreSlim _semaphoreSpot = new SemaphoreSlim(1, 1); + private static readonly SemaphoreSlim _semaphoreFutures = new SemaphoreSlim(1, 1); + + //public static async Task UpdateFuturesSymbolInfoAsync() + //{ + // await _semaphoreFutures.WaitAsync().ConfigureAwait(false); + // if (DateTime.UtcNow - _lastUpdateTime < TimeSpan.FromHours(1)) + // return new CallResult(null); + + // try + // { + // var symbolInfo = await new HyperLiquidRestClient().Api.ExchangeData.Get().ConfigureAwait(false); + // if (!symbolInfo) + // return symbolInfo.AsDataless(); + + // _symbolInfo = symbolInfo.Data.Symbols; + // _lastUpdateTime = DateTime.UtcNow; + // return new CallResult(null); + // } + // finally + // { + // _semaphoreFutures.Release(); + // } + //} + + public static async Task UpdateSpotSymbolInfoAsync() + { + await _semaphoreSpot.WaitAsync().ConfigureAwait(false); + if (DateTime.UtcNow - _lastUpdateTime < TimeSpan.FromHours(1)) + return new CallResult(null); + + try + { + var symbolInfo = await new HyperLiquidRestClient().Api.ExchangeData.GetSpotExchangeInfoAsync().ConfigureAwait(false); + if (!symbolInfo) + return symbolInfo.AsDataless(); + + _spotSymbolInfo = symbolInfo.Data.Symbols; + _lastUpdateTime = DateTime.UtcNow; + return new CallResult(null); + } + finally + { + _semaphoreSpot.Release(); + } + } + + public static async Task> GetSymbolIdFromName(SymbolType type, string symbolName) + { + if (type == SymbolType.Spot) + { + var update = await UpdateSpotSymbolInfoAsync().ConfigureAwait(false); + if (!update) + return new CallResult(update.Error!); + } + else + { + + } + + var symbol = _spotSymbolInfo.SingleOrDefault(x => x.Name == symbolName); + if (symbol == null) + return new CallResult(new ServerError("Symbol not found")); + + return new CallResult(symbol.Index + (type == SymbolType.Spot ? 10000 : 0)); + } + } +} diff --git a/HyperLiquid.Net/PackConverter.cs b/HyperLiquid.Net/Utils/PackConverter.cs similarity index 94% rename from HyperLiquid.Net/PackConverter.cs rename to HyperLiquid.Net/Utils/PackConverter.cs index 16e6ca4..d16e5ec 100644 --- a/HyperLiquid.Net/PackConverter.cs +++ b/HyperLiquid.Net/Utils/PackConverter.cs @@ -4,7 +4,7 @@ using System.IO; using System.Text; -namespace HyperLiquid.Net +namespace HyperLiquid.Net.Utils { /// /// Message pack converter @@ -74,9 +74,7 @@ private void Pack(Stream s, IList list) { int count = list.Count; if (count < 16) - { s.WriteByte((byte)(0x90 + count)); - } else if (count < 0x10000) { s.WriteByte(0xdc); @@ -97,9 +95,7 @@ private void Pack(Stream s, IDictionary dict) { int count = dict.Count; if (count < 16) - { s.WriteByte((byte)(0x80 + count)); - } else if (count < 0x10000) { s.WriteByte(0xde); @@ -127,9 +123,7 @@ private void Pack(Stream s, sbyte val) unchecked { if (val >= -32) - { s.WriteByte((byte)val); - } else { tmp0[0] = 0xd0; @@ -142,9 +136,7 @@ private void Pack(Stream s, sbyte val) private void Pack(Stream s, byte val) { if (val <= 0x7f) - { s.WriteByte(val); - } else { tmp0[0] = 0xcc; @@ -158,9 +150,7 @@ private void Pack(Stream s, short val) unchecked { if (val >= 0) - { Pack(s, (ushort)val); - } else if (val >= -128) { Pack(s, (sbyte)val); @@ -178,9 +168,7 @@ private void Pack(Stream s, ushort val) unchecked { if (val < 0x100) - { Pack(s, (byte)val); - } else { s.WriteByte(0xcd); @@ -194,9 +182,7 @@ private void Pack(Stream s, int val) unchecked { if (val >= 0) - { Pack(s, (uint)val); - } else if (val >= -128) { Pack(s, (sbyte)val); @@ -219,9 +205,7 @@ private void Pack(Stream s, uint val) unchecked { if (val < 0x100) - { Pack(s, (byte)val); - } else if (val < 0x10000) { s.WriteByte(0xcd); @@ -240,9 +224,7 @@ private void Pack(Stream s, long val) unchecked { if (val >= 0) - { Pack(s, (ulong)val); - } else if (val >= -128) { Pack(s, (sbyte)val); @@ -270,9 +252,7 @@ private void Pack(Stream s, ulong val) unchecked { if (val < 0x100) - { Pack(s, (byte)val); - } else if (val < 0x10000) { s.WriteByte(0xcd); @@ -335,23 +315,21 @@ private void Pack(Stream s, string val) { var bytes = encoder.GetBytes(val); if (bytes.Length < 0x20) - { s.WriteByte((byte)(0xa0 + bytes.Length)); - } else if (bytes.Length < 0x100) { s.WriteByte(0xd9); - s.WriteByte((byte)(bytes.Length)); + s.WriteByte((byte)bytes.Length); } else if (bytes.Length < 0x10000) { s.WriteByte(0xda); - Write(s, (ushort)(bytes.Length)); + Write(s, (ushort)bytes.Length); } else { s.WriteByte(0xdb); - Write(s, (uint)(bytes.Length)); + Write(s, (uint)bytes.Length); } s.Write(bytes, 0, bytes.Length); }