From ff38021071c2245ebe72aa1519c6424a2e492d57 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Sat, 24 Feb 2024 22:37:38 +0100 Subject: [PATCH] Websocket refactoring (#103) Websocket refactoring --- Huobi.Net.UnitTests/HuobiClientTests.cs | 7 +- Huobi.Net.UnitTests/HuobiSocketClientTests.cs | 110 +-- .../TestImplementations/TestHelpers.cs | 1 + .../TestImplementations/TestSocket.cs | 28 +- Huobi.Net/Clients/HuobiRestClient.cs | 9 +- .../HuobiRestClientSpotApiExchangeData.cs | 1 + .../SpotApi/HuobiRestClientSpotApiTrading.cs | 1 + .../SpotApi/HuobiSocketClientSpotApi.cs | 690 +++--------------- .../HuobiClientUsdtMarginSwapApi.cs | 2 - .../HuobiClientUsdtMarginSwapApiAccount.cs | 1 - ...uobiClientUsdtMarginSwapApiExchangeData.cs | 2 - .../HuobiClientUsdtMarginSwapApiTrading.cs | 2 - .../HuobiSocketClientUsdtMarginSwapApi.cs | 607 +++------------ .../CryptoClientExtensions.cs | 25 + .../ExtensionMethods/HuobiExtensionMethods.cs | 26 + .../ServiceCollectionExtensions.cs} | 38 +- Huobi.Net/Huobi.Net.csproj | 14 +- Huobi.Net/Huobi.Net.xml | 217 +++--- Huobi.Net/HuobiAuthenticationProvider.cs | 7 +- Huobi.Net/Interfaces/Clients/IHuobiClient.cs | 1 - .../SpotApi/IHuobiSocketClientSpotApi.cs | 8 +- .../IHuobiSocketClientUsdtMarginSwapApi.cs | 6 +- .../Objects/Internal/HuobiSocketUpdate.cs | 5 + .../HuobiConditionalOrderCancelResult.cs | 1 - Huobi.Net/Objects/Models/HuobiOpenOrder.cs | 1 - Huobi.Net/Objects/Models/HuobiOrder.cs | 1 - Huobi.Net/Objects/Models/HuobiOrderBook.cs | 1 - Huobi.Net/Objects/Models/HuobiRepayment.cs | 2 - Huobi.Net/Objects/Models/HuobiSymbolTrade.cs | 1 - .../Models/Socket/HuobiOrderBookUpdate.cs | 1 - .../Socket/HuobiUsdtMarginSwapTradeUpdate.cs | 1 - .../Models/UsdtMarginSwap/HuobiBatchResult.cs | 2 - .../HuobiEstimatedSettlementPrice.cs | 3 - .../HuobiIsolatedMarginAssetsAndPositions.cs | 1 - .../UsdtMarginSwap/HuobiPriceLimitation.cs | 3 - .../Models/UsdtMarginSwap/HuobiTradingFee.cs | 3 - .../HuobiAuthParams.cs} | 17 +- .../Objects/Sockets/HuobiAuthPongMessage.cs | 32 + Huobi.Net/Objects/Sockets/HuobiAuthRequest.cs | 18 + Huobi.Net/Objects/Sockets/HuobiPingMessage.cs | 22 + Huobi.Net/Objects/Sockets/HuobiPongMessage.cs | 18 + .../Sockets/HuobiSocketAuthResponse.cs | 16 + .../Objects/Sockets/HuobiSocketResponse.cs | 14 + .../Objects/Sockets/HuobiSubscribeRequest.cs | 12 + .../Sockets/HuobiUnsubscribeRequest.cs | 12 + .../Objects/Sockets/Queries/HuobiAuthQuery.cs | 29 + .../Objects/Sockets/Queries/HuobiQuery.cs | 28 + .../Sockets/Queries/HuobiSubscribeQuery.cs | 27 + .../Sockets/Queries/HuobiUnsubscribeQuery.cs | 17 + .../Subscriptions/HuobiAccountSubscription.cs | 46 ++ .../HuobiAuthPingSubscription.cs | 25 + .../HuobiOrderDetailsSubscription.cs | 66 ++ .../Subscriptions/HuobiOrderSubscription.cs | 87 +++ .../Subscriptions/HuobiPingSubscription.cs | 25 + .../HuobiSpotPingSubscription.cs | 25 + .../Subscriptions/HuobiSubscription.cs | 46 ++ .../SymbolOrderBooks/HuobiOrderBookFactory.cs | 1 - .../HuobiSpotSymbolOrderBook.cs | 12 +- README.md | 123 +++- 59 files changed, 1166 insertions(+), 1381 deletions(-) create mode 100644 Huobi.Net/ExtensionMethods/CryptoClientExtensions.cs create mode 100644 Huobi.Net/ExtensionMethods/HuobiExtensionMethods.cs rename Huobi.Net/{HuobiHelpers.cs => ExtensionMethods/ServiceCollectionExtensions.cs} (73%) rename Huobi.Net/Objects/{Internal/HuobiAuthenticationRequest.cs => Sockets/HuobiAuthParams.cs} (53%) create mode 100644 Huobi.Net/Objects/Sockets/HuobiAuthPongMessage.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiAuthRequest.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiPingMessage.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiPongMessage.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiSocketAuthResponse.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiSocketResponse.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiSubscribeRequest.cs create mode 100644 Huobi.Net/Objects/Sockets/HuobiUnsubscribeRequest.cs create mode 100644 Huobi.Net/Objects/Sockets/Queries/HuobiAuthQuery.cs create mode 100644 Huobi.Net/Objects/Sockets/Queries/HuobiQuery.cs create mode 100644 Huobi.Net/Objects/Sockets/Queries/HuobiSubscribeQuery.cs create mode 100644 Huobi.Net/Objects/Sockets/Queries/HuobiUnsubscribeQuery.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiAccountSubscription.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiAuthPingSubscription.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderDetailsSubscription.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderSubscription.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiPingSubscription.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiSpotPingSubscription.cs create mode 100644 Huobi.Net/Objects/Sockets/Subscriptions/HuobiSubscription.cs diff --git a/Huobi.Net.UnitTests/HuobiClientTests.cs b/Huobi.Net.UnitTests/HuobiClientTests.cs index 863a080f..545185f1 100644 --- a/Huobi.Net.UnitTests/HuobiClientTests.cs +++ b/Huobi.Net.UnitTests/HuobiClientTests.cs @@ -2,18 +2,15 @@ using Newtonsoft.Json; using NUnit.Framework; using System; -using System.Collections.Generic; using System.Linq; -using System.Net.Http; -using CryptoExchange.Net.Authentication; using System.Threading.Tasks; -using Huobi.Net.Enums; using System.Reflection; using System.Diagnostics; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; using Huobi.Net.Clients; using Huobi.Net.Clients.SpotApi; +using Huobi.Net.ExtensionMethods; +using CryptoExchange.Net.Objects.Sockets; namespace Huobi.Net.UnitTests { diff --git a/Huobi.Net.UnitTests/HuobiSocketClientTests.cs b/Huobi.Net.UnitTests/HuobiSocketClientTests.cs index 153f768a..ad9d20db 100644 --- a/Huobi.Net.UnitTests/HuobiSocketClientTests.cs +++ b/Huobi.Net.UnitTests/HuobiSocketClientTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using CryptoExchange.Net; using Huobi.Net.Enums; using Huobi.Net.Objects; @@ -18,7 +19,7 @@ namespace Huobi.Net.UnitTests public class HuobiSocketClientTests { [Test] - public void SubscribeV1_Should_SucceedIfSubbedResponse() + public async Task SubscribeV1_Should_SucceedIfSubbedResponse() { // arrange var socket = new TestSocket(); @@ -28,7 +29,7 @@ public void SubscribeV1_Should_SucceedIfSubbedResponse() // act var subTask = client.SpotApi.SubscribeToPartialOrderBookUpdates1SecondAsync("ETHBTC", 1, test => { }); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"subbed\": \"test\", \"id\":\"{id}\", \"status\": \"ok\"}}"); + await socket.InvokeMessage($"{{\"subbed\": \"test\", \"id\":\"{id}\", \"status\": \"ok\"}}"); var subResult = subTask.Result; // assert @@ -36,7 +37,7 @@ public void SubscribeV1_Should_SucceedIfSubbedResponse() } [Test] - public void SubscribeV1_Should_FailIfNoResponse() + public async Task SubscribeV1_Should_FailIfNoResponse() { // arrange var socket = new TestSocket(); @@ -47,15 +48,14 @@ public void SubscribeV1_Should_FailIfNoResponse() }); // act - var subTask = client.SpotApi.SubscribeToPartialOrderBookUpdates1SecondAsync("ETHBTC", 1, test => { }); - var subResult = subTask.Result; + var subResult = await client.SpotApi.SubscribeToPartialOrderBookUpdates1SecondAsync("ETHBTC", 1, test => { }); // assert Assert.IsFalse(subResult.Success); } [Test] - public void SubscribeV1_Should_FailIfErrorResponse() + public async Task SubscribeV1_Should_FailIfErrorResponse() { // arrange var socket = new TestSocket(); @@ -65,7 +65,7 @@ public void SubscribeV1_Should_FailIfErrorResponse() // act var subTask = client.SpotApi.SubscribeToPartialOrderBookUpdates1SecondAsync("ETHBTC", 1, test => { }); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"status\": \"error\", \"id\": \"{id}\", \"err-code\": \"Fail\", \"err-msg\": \"failed\"}}"); + await socket.InvokeMessage($"{{\"status\": \"error\", \"id\": \"{id}\", \"err-code\": \"Fail\", \"err-msg\": \"failed\"}}"); var subResult = subTask.Result; // assert @@ -73,7 +73,7 @@ public void SubscribeV1_Should_FailIfErrorResponse() } [Test] - public void SubscribeToDepthUpdates_Should_TriggerWithDepthUpdate() + public async Task SubscribeToDepthUpdates_Should_TriggerWithDepthUpdate() { // arrange var socket = new TestSocket(); @@ -83,7 +83,7 @@ public void SubscribeToDepthUpdates_Should_TriggerWithDepthUpdate() HuobiOrderBook result = null; var subTask = client.SpotApi.SubscribeToPartialOrderBookUpdates1SecondAsync("ETHBTC", 1, test => result = test.Data); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"subbed\": \"ethbtc\", \"status\": \"ok\", \"id\": \"{id}\"}}"); + await socket.InvokeMessage($"{{\"subbed\": \"ethbtc\", \"status\": \"ok\", \"id\": \"{id}\"}}"); var subResult = subTask.Result; var expected = new HuobiOrderBook() @@ -99,7 +99,7 @@ public void SubscribeToDepthUpdates_Should_TriggerWithDepthUpdate() }; // act - socket.InvokeMessage(SerializeExpected("market.ethbtc.depth.step1", expected)); + await socket.InvokeMessage(SerializeExpected("market.ethbtc.depth.step1", expected)); // assert Assert.IsTrue(subResult.Success); @@ -108,17 +108,17 @@ public void SubscribeToDepthUpdates_Should_TriggerWithDepthUpdate() } [Test] - public void SubscribeToDetailUpdates_Should_TriggerWithDetailUpdate() + public async Task SubscribeToDetailUpdates_Should_TriggerWithDetailUpdate() { // arrange var socket = new TestSocket(); socket.CanConnect = true; var client = TestHelpers.CreateSocketClient(socket); - HuobiSymbolData result = null; + HuobiSymbolDetails result = null; var subTask = client.SpotApi.SubscribeToSymbolDetailUpdatesAsync("ETHBTC", test => result = test.Data); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"subbed\": \"ethbtc\", \"id\": \"{id}\", \"status\": \"ok\"}}"); + await socket.InvokeMessage($"{{\"subbed\": \"ethbtc\", \"id\": \"{id}\", \"status\": \"ok\"}}"); var subResult = subTask.Result; var expected = new HuobiSymbolData() @@ -133,7 +133,7 @@ public void SubscribeToDetailUpdates_Should_TriggerWithDetailUpdate() }; // act - socket.InvokeMessage(SerializeExpected("market.ethbtc.detail", expected)); + await socket.InvokeMessage(SerializeExpected("market.ethbtc.detail", expected)); // assert Assert.IsTrue(subResult.Success); @@ -141,7 +141,7 @@ public void SubscribeToDetailUpdates_Should_TriggerWithDetailUpdate() } [Test] - public void SubscribeToKlineUpdates_Should_TriggerWithKlineUpdate() + public async Task SubscribeToKlineUpdates_Should_TriggerWithKlineUpdate() { // arrange var socket = new TestSocket(); @@ -151,7 +151,7 @@ public void SubscribeToKlineUpdates_Should_TriggerWithKlineUpdate() HuobiSymbolData result = null; var subTask = client.SpotApi.SubscribeToKlineUpdatesAsync("ETHBTC", KlineInterval.FiveMinutes, test => result = test.Data); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"subbed\": \"ethbtc\", \"id\": \"{id}\", \"status\": \"ok\"}}"); + await socket.InvokeMessage($"{{\"subbed\": \"ethbtc\", \"id\": \"{id}\", \"status\": \"ok\"}}"); var subResult = subTask.Result; var expected = new HuobiSymbolData() @@ -166,7 +166,7 @@ public void SubscribeToKlineUpdates_Should_TriggerWithKlineUpdate() }; // act - socket.InvokeMessage(SerializeExpected("market.ethbtc.kline.5min", expected)); + await socket.InvokeMessage(SerializeExpected("market.ethbtc.kline.5min", expected)); // assert Assert.IsTrue(subResult.Success); @@ -174,22 +174,22 @@ public void SubscribeToKlineUpdates_Should_TriggerWithKlineUpdate() } [Test] - public void SubscribeToTickerUpdates_Should_TriggerWithTickerUpdate() + public async Task SubscribeToTickerUpdates_Should_TriggerWithTickerUpdate() { // arrange var socket = new TestSocket(); socket.CanConnect = true; var client = TestHelpers.CreateSocketClient(socket); - HuobiSymbolDatas result = null; + IEnumerable result = null; var subTask = client.SpotApi.SubscribeToTickerUpdatesAsync((test => result = test.Data)); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"subbed\": \"test\", \"id\": \"{id}\", \"status\": \"ok\"}}"); + await socket.InvokeMessage($"{{\"subbed\": \"test\", \"id\": \"{id}\", \"status\": \"ok\"}}"); var subResult = subTask.Result; - var expected = new List + var expected = new List { - new HuobiSymbolData() + new HuobiSymbolTicker() { QuoteVolume = 0.1m, ClosePrice = 0.2m, @@ -202,15 +202,15 @@ public void SubscribeToTickerUpdates_Should_TriggerWithTickerUpdate() }; // act - socket.InvokeMessage(SerializeExpected("market.tickers", expected)); + await socket.InvokeMessage(SerializeExpected("market.tickers", expected)); // assert Assert.IsTrue(subResult.Success); - Assert.IsTrue(TestHelpers.AreEqual(expected[0], result.Ticks.ToList()[0])); + Assert.IsTrue(TestHelpers.AreEqual(expected[0], result.First())); } [Test] - public void SubscribeToTradeUpdates_Should_TriggerWithTradeUpdate() + public async Task SubscribeToTradeUpdates_Should_TriggerWithTradeUpdate() { // arrange var socket = new TestSocket(); @@ -220,7 +220,7 @@ public void SubscribeToTradeUpdates_Should_TriggerWithTradeUpdate() HuobiSymbolTrade result = null; var subTask = client.SpotApi.SubscribeToTradeUpdatesAsync("ethusdt", test => result = test.Data); var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"subbed\": \"test\", \"id\": \"{id}\", \"status\": \"ok\"}}"); + await socket.InvokeMessage($"{{\"subbed\": \"test\", \"id\": \"{id}\", \"status\": \"ok\"}}"); var subResult = subTask.Result; var expected = @@ -242,7 +242,7 @@ public void SubscribeToTradeUpdates_Should_TriggerWithTradeUpdate() }; // act - socket.InvokeMessage(SerializeExpected("market.ethusdt.trade.detail", expected)); + await socket.InvokeMessage(SerializeExpected("market.ethusdt.trade.detail", expected)); // assert Assert.IsTrue(subResult.Success); @@ -251,7 +251,7 @@ public void SubscribeToTradeUpdates_Should_TriggerWithTradeUpdate() } [Test] - public void SubscribeToAccountUpdates_Should_TriggerWithAccountUpdate() + public async Task SubscribeToAccountUpdates_Should_TriggerWithAccountUpdate() { // arrange var socket = new TestSocket(); @@ -260,9 +260,9 @@ public void SubscribeToAccountUpdates_Should_TriggerWithAccountUpdate() HuobiAccountUpdate result = null; var subTask = client.SpotApi.SubscribeToAccountUpdatesAsync(test => result = test.Data); - socket.InvokeMessage("{\"ch\": \"auth\", \"code\": 200, \"action\": \"req\"}"); + await socket.InvokeMessage("{\"ch\": \"auth\", \"code\": 200, \"action\": \"req\"}"); Thread.Sleep(100); - socket.InvokeMessage($"{{\"action\": \"sub\", \"code\": 200, \"ch\": \"accounts.update#1\"}}"); + await socket.InvokeMessage($"{{\"action\": \"sub\", \"code\": 200, \"ch\": \"accounts.update#1\"}}"); var subResult = subTask.Result; var expected = new HuobiAccountUpdate() @@ -277,7 +277,7 @@ public void SubscribeToAccountUpdates_Should_TriggerWithAccountUpdate() }; // act - socket.InvokeMessage(SerializeExpectedAuth("accounts.update#1", expected)); + await socket.InvokeMessage(SerializeExpectedAuth("accounts.update#1", expected)); // assert Assert.IsTrue(subResult.Success); @@ -285,7 +285,7 @@ public void SubscribeToAccountUpdates_Should_TriggerWithAccountUpdate() } [Test] - public void SubscribeV2_Should_SucceedIfSubbedResponse() + public async Task SubscribeV2_Should_SucceedIfSubbedResponse() { // arrange var socket = new TestSocket(); @@ -294,9 +294,9 @@ public void SubscribeV2_Should_SucceedIfSubbedResponse() // act var subTask = client.SpotApi.SubscribeToAccountUpdatesAsync(test => { }); - socket.InvokeMessage("{\"action\": \"req\", \"code\": 200, \"ch\": \"auth\"}"); + await socket.InvokeMessage("{\"action\": \"req\", \"code\": 200, \"ch\": \"auth\"}"); Thread.Sleep(10); - socket.InvokeMessage("{\"action\": \"sub\", \"code\": 200, \"ch\": \"accounts.update#1\"}"); + await socket.InvokeMessage("{\"action\": \"sub\", \"code\": 200, \"ch\": \"accounts.update#1\"}"); var subResult = subTask.Result; // assert @@ -304,7 +304,7 @@ public void SubscribeV2_Should_SucceedIfSubbedResponse() } [Test] - public void SubscribeV2_Should_FailIfAuthErrorResponse() + public async Task SubscribeV2_Should_FailIfAuthErrorResponse() { // arrange var socket = new TestSocket(); @@ -313,32 +313,32 @@ public void SubscribeV2_Should_FailIfAuthErrorResponse() // act var subTask = client.SpotApi.SubscribeToAccountUpdatesAsync(test => { }); - socket.InvokeMessage("{ \"action\": \"req\", \"ch\": \"auth\", \"code\": 400}"); + await socket.InvokeMessage("{ \"action\": \"req\", \"ch\": \"auth\", \"code\": 400}"); var subResult = subTask.Result; // assert Assert.IsFalse(subResult.Success); } - [Test] - public void SubscribeV2_Should_FailIfErrorResponse() - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateSocketClient(socket); - - // act - var subTask = client.SpotApi.SubscribeToAccountUpdatesAsync(test => { }); - socket.InvokeMessage("{\"op\": \"auth\"}"); - Thread.Sleep(10); - var id = JToken.Parse(socket.LastSendMessage)["id"]; - socket.InvokeMessage($"{{\"op\": \"sub\", \"cid\": \"{id}\", \"status\": \"error\", \"err-code\": 1, \"err-msg\": \"failed\"}}"); - var subResult = subTask.Result; - - // assert - Assert.IsFalse(subResult.Success); - } + //[Test] + //public async Task SubscribeV2_Should_FailIfErrorResponse() + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateSocketClient(socket); + + // // act + // var subTask = client.SpotApi.SubscribeToAccountUpdatesAsync(test => { }); + // await socket.InvokeMessage("{\"op\": \"auth\"}"); + // Thread.Sleep(10); + // var id = JToken.Parse(socket.LastSendMessage)["id"]; + // await socket.InvokeMessage($"{{\"op\": \"sub\", \"cid\": \"{id}\", \"status\": \"error\", \"err-code\": 1, \"err-msg\": \"failed\"}}"); + // var subResult = subTask.Result; + + // // assert + // Assert.IsFalse(subResult.Success); + //} [Test] public void SubscribeV2_Should_FailIfNoResponse() diff --git a/Huobi.Net.UnitTests/TestImplementations/TestHelpers.cs b/Huobi.Net.UnitTests/TestImplementations/TestHelpers.cs index dcbd0eb8..af121cc7 100644 --- a/Huobi.Net.UnitTests/TestImplementations/TestHelpers.cs +++ b/Huobi.Net.UnitTests/TestImplementations/TestHelpers.cs @@ -12,6 +12,7 @@ using CryptoExchange.Net; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; using Huobi.Net.Clients; using Huobi.Net.Enums; diff --git a/Huobi.Net.UnitTests/TestImplementations/TestSocket.cs b/Huobi.Net.UnitTests/TestImplementations/TestSocket.cs index 81d8cf8e..cab7b5a0 100644 --- a/Huobi.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/Huobi.Net.UnitTests/TestImplementations/TestSocket.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using System.Net.WebSockets; using System.Security.Authentication; using System.Text; using System.Threading.Tasks; @@ -10,18 +12,18 @@ namespace Huobi.Net.UnitTests.TestImplementations { public class TestSocket: IWebsocket { - public bool CanConnect { get; set; } + public bool CanConnect { get; set; } = true; public bool Connected { get; set; } - public event Action OnClose; - public event Action OnMessage; - public event Action OnError; - public event Action OnOpen; - public event Action OnRequestSent; + public event Func OnClose; #pragma warning disable 0067 - public event Action OnReconnecting; - public event Action OnReconnected; + public event Func OnReconnected; + public event Func OnReconnecting; #pragma warning restore 0067 + public event Func OnRequestSent; + public event Func OnStreamMessage; + public event Func OnError; + public event Func OnOpen; public int Id { get; } public bool ShouldReconnect { get; set; } @@ -91,14 +93,16 @@ public void InvokeOpen() OnOpen?.Invoke(); } - public void InvokeMessage(string data) + public async Task InvokeMessage(string data) { - OnMessage?.Invoke(data); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); + await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream); } - public void InvokeMessage(T data) + public async Task InvokeMessage(T data) { - OnMessage?.Invoke(JsonConvert.SerializeObject(data)); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data))); + await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream); } public void InvokeError(Exception error) diff --git a/Huobi.Net/Clients/HuobiRestClient.cs b/Huobi.Net/Clients/HuobiRestClient.cs index 8aff0a40..e192e679 100644 --- a/Huobi.Net/Clients/HuobiRestClient.cs +++ b/Huobi.Net/Clients/HuobiRestClient.cs @@ -30,14 +30,7 @@ public class HuobiRestClient : BaseRestClient, IHuobiRestClient /// Create a new instance of the HuobiRestClient using provided options /// /// Option configuration delegate - public HuobiRestClient(Action optionsDelegate) : this(null, null, optionsDelegate) - { - } - - /// - /// Create a new instance of the HuobiRestClient using default options - /// - public HuobiRestClient(ILoggerFactory? loggerFactory = null, HttpClient? httpClient = null) : this(httpClient, loggerFactory, null) + public HuobiRestClient(Action? optionsDelegate = null) : this(null, null, optionsDelegate) { } diff --git a/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiExchangeData.cs b/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiExchangeData.cs index d358e9ea..a3e6d7c6 100644 --- a/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiExchangeData.cs +++ b/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiExchangeData.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Huobi.Net.Objects.Models; using Huobi.Net.Interfaces.Clients.SpotApi; +using Huobi.Net.ExtensionMethods; namespace Huobi.Net.Clients.SpotApi { diff --git a/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiTrading.cs b/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiTrading.cs index 163a9605..e8974446 100644 --- a/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiTrading.cs +++ b/Huobi.Net/Clients/SpotApi/HuobiRestClientSpotApiTrading.cs @@ -14,6 +14,7 @@ using CryptoExchange.Net.Converters; using Huobi.Net.Interfaces.Clients.SpotApi; using CryptoExchange.Net.CommonObjects; +using Huobi.Net.ExtensionMethods; namespace Huobi.Net.Clients.SpotApi { diff --git a/Huobi.Net/Clients/SpotApi/HuobiSocketClientSpotApi.cs b/Huobi.Net/Clients/SpotApi/HuobiSocketClientSpotApi.cs index 8f0c469c..ba8934c0 100644 --- a/Huobi.Net/Clients/SpotApi/HuobiSocketClientSpotApi.cs +++ b/Huobi.Net/Clients/SpotApi/HuobiSocketClientSpotApi.cs @@ -1,25 +1,30 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.IO.Compression; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.MessageParsing; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Huobi.Net.Converters; using Huobi.Net.Enums; +using Huobi.Net.ExtensionMethods; using Huobi.Net.Interfaces.Clients.SpotApi; -using Huobi.Net.Objects; using Huobi.Net.Objects.Internal; using Huobi.Net.Objects.Models; using Huobi.Net.Objects.Models.Socket; using Huobi.Net.Objects.Options; +using Huobi.Net.Objects.Sockets; +using Huobi.Net.Objects.Sockets.Queries; +using Huobi.Net.Objects.Sockets.Subscriptions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using HuobiOrderUpdate = Huobi.Net.Objects.Models.Socket.HuobiOrderUpdate; namespace Huobi.Net.Clients.SpotApi @@ -27,6 +32,11 @@ namespace Huobi.Net.Clients.SpotApi /// public class HuobiSocketClientSpotApi : SocketApiClient, IHuobiSocketClientSpotApi { + private static readonly MessagePath _idPath = MessagePath.Get().Property("id"); + private static readonly MessagePath _actionPath = MessagePath.Get().Property("action"); + private static readonly MessagePath _channelPath = MessagePath.Get().Property("ch"); + private static readonly MessagePath _pingPath = MessagePath.Get().Property("ping"); + #region fields #endregion @@ -36,26 +46,69 @@ internal HuobiSocketClientSpotApi(ILogger logger, HuobiSocketOptions options) { KeepAliveInterval = TimeSpan.Zero; - SetDataInterpreter(DecompressData, null); - AddGenericHandler("PingV1", PingHandlerV1); - AddGenericHandler("PingV2", PingHandlerV2); - AddGenericHandler("PingV3", PingHandlerV3); + AddSystemSubscription(new HuobiSpotPingSubscription(_logger)); + AddSystemSubscription(new HuobiPingSubscription(_logger)); } #endregion + /// + public override string? GetListenerIdentifier(IMessageAccessor message) + { + var id = message.GetValue(_idPath); + if (id != null) + return id; + + var action = message.GetValue(_actionPath); + if (action == "ping") + return "pingV2"; + + var ping = message.GetValue(_pingPath); + if (ping != null) + return "pingV3"; + + var channel = message.GetValue(_channelPath); + if (action != null && action != "push") + return action + channel; + + return channel; + } + + /// + public override Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) + { + if (type != WebSocketMessageType.Binary) + return stream; + + var decompressedStream = new MemoryStream(); + using var deflateStream = new GZipStream(stream, CompressionMode.Decompress); + deflateStream.CopyTo(decompressedStream); + decompressedStream.Position = 0; + return decompressedStream; + } + /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => new HuobiAuthenticationProvider(credentials, false); - #region methods + /// + protected override Query GetAuthenticationRequest() + { + return new HuobiAuthQuery(new HuobiAuthRequest + { + Action = "req", + Channel = "auth", + Params = ((HuobiAuthenticationProvider)AuthenticationProvider!).GetWebsocketAuthentication(new Uri(BaseAddress.AppendPath("ws/v2"))) + }); + } /// public async Task>> GetKlinesAsync(string symbol, KlineInterval period) { symbol = symbol.ValidateHuobiSymbol(); - var request = new HuobiSocketRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.kline.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var result = await QueryAsync>>(BaseAddress.AppendPath("ws"), request, false).ConfigureAwait(false); + + var query = new HuobiQuery>($"market.{symbol}.kline.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", false); + var result = await QueryAsync(BaseAddress.AppendPath("ws"), query).ConfigureAwait(false); return result ? result.As(result.Data.Data) : result.AsError>(result.Error!); } @@ -63,9 +116,9 @@ public async Task>> GetKlinesAsync(string sym public async Task> SubscribeToKlineUpdatesAsync(string symbol, KlineInterval period, Action> onData, CancellationToken ct = default) { symbol = symbol.ValidateHuobiSymbol(); - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.kline.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, symbol))); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.kline.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// @@ -74,13 +127,9 @@ public async Task> GetOrderBookWithMergeStepAsync(str symbol = symbol.ValidateHuobiSymbol(); mergeStep.ValidateIntBetween(nameof(mergeStep), 0, 5); - var request = new HuobiSocketRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.depth.step{mergeStep}"); - var result = await QueryAsync>(BaseAddress.AppendPath("ws"), request, false).ConfigureAwait(false); - if (!result) - return new CallResult(result.Error!); - - result.Data.Data.Timestamp = result.Data.Timestamp; - return new CallResult(result.Data.Data); + var query = new HuobiQuery($"market.{symbol}.depth.step{mergeStep}", false); + var result = await QueryAsync(BaseAddress.AppendPath("ws"), query).ConfigureAwait(false); + return result ? result.As(result.Data.Data) : result.AsError(result.Error!); } /// @@ -89,20 +138,9 @@ public async Task> GetOrderBookAsync(string symbol = symbol.ValidateHuobiSymbol(); levels.ValidateIntValues(nameof(levels), 5, 20, 150, 400); - var request = new HuobiSocketRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.mbp.{levels}"); - var result = await QueryAsync>(BaseAddress.AppendPath("feed"), request, false).ConfigureAwait(false); - if (!result) - return new CallResult(result.Error!); - - if (result.Data.Data == null) - { - var info = "No data received when requesting order book. " + - "Levels 5/20 are only supported for a subset of symbols, see https://huobiapi.github.io/docs/spot/v1/en/#market-by-price-incremental-update. Use 150 level instead."; - _logger.Log(LogLevel.Debug, info); - return new CallResult(new ServerError(info)); - } - - return new CallResult(result.Data.Data); + var query = new HuobiQuery($"market.{symbol}.mbp.{levels}", false); + var result = await QueryAsync(BaseAddress.AppendPath("feed"), query).ConfigureAwait(false); + return result ? result.As(result.Data.Data) : result.AsError(result.Error!); } /// @@ -111,14 +149,8 @@ public async Task> SubscribeToPartialOrderBookUpd symbol = symbol.ValidateHuobiSymbol(); mergeStep.ValidateIntBetween(nameof(mergeStep), 0, 5); - var internalHandler = new Action>>(data => - { - data.Data.Timestamp = data.Timestamp; - onData(data.As(data.Data.Data, symbol)); - }); - - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.depth.step{mergeStep}"); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.depth.step{mergeStep}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// @@ -127,14 +159,8 @@ public async Task> SubscribeToPartialOrderBookUpd symbol = symbol.ValidateHuobiSymbol(); levels.ValidateIntValues(nameof(levels), 5, 10, 20); - var internalHandler = new Action>>(data => - { - data.Data.Timestamp = data.Timestamp; - onData(data.As(data.Data.Data, symbol)); - }); - - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.mbp.refresh.{levels}"); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.mbp.refresh.{levels}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// @@ -143,22 +169,17 @@ public async Task> SubscribeToOrderBookChangeUpda symbol = symbol.ValidateHuobiSymbol(); levels.ValidateIntValues(nameof(levels), 5, 20, 150, 400); - var internalHandler = new Action>>(data => - { - data.Data.Timestamp = data.Timestamp; - onData(data.As(data.Data.Data, symbol)); - }); - - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.mbp.{levels}"); - return await SubscribeAsync(BaseAddress.AppendPath("feed"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.mbp.{levels}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("feed"), subscription, ct).ConfigureAwait(false); } /// public async Task>> GetTradeHistoryAsync(string symbol) { symbol = symbol.ValidateHuobiSymbol(); - var request = new HuobiSocketRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.trade.detail"); - var result = await QueryAsync>>(BaseAddress.AppendPath("ws"), request, false).ConfigureAwait(false); + + var query = new HuobiQuery>($"market.{symbol}.trade.detail", false); + var result = await QueryAsync(BaseAddress.AppendPath("ws"), query).ConfigureAwait(false); return result ? result.As(result.Data.Data) : result.AsError>(result.Error!); } @@ -166,17 +187,17 @@ public async Task>> GetTradeHist public async Task> SubscribeToTradeUpdatesAsync(string symbol, Action> onData, CancellationToken ct = default) { symbol = symbol.ValidateHuobiSymbol(); - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.trade.detail"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, symbol))); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.trade.detail", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> GetSymbolDetailsAsync(string symbol) { symbol = symbol.ValidateHuobiSymbol(); - var request = new HuobiSocketRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.detail"); - var result = await QueryAsync>(BaseAddress.AppendPath("ws"), request, false).ConfigureAwait(false); + + var query = new HuobiQuery($"market.{symbol}.detail", false); + var result = await QueryAsync(BaseAddress.AppendPath("ws"), query).ConfigureAwait(false); if (!result) return result.AsError(result.Error!); @@ -188,48 +209,29 @@ public async Task> GetSymbolDetailsAsync(string s public async Task> SubscribeToSymbolDetailUpdatesAsync(string symbol, Action> onData, CancellationToken ct = default) { symbol = symbol.ValidateHuobiSymbol(); - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.detail"); - var internalHandler = new Action>>(data => - { - data.Data.Timestamp = data.Timestamp; - onData(data.As(data.Data.Data, symbol)); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.detail", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// - public async Task> SubscribeToTickerUpdatesAsync(Action> onData, CancellationToken ct = default) + public async Task> SubscribeToTickerUpdatesAsync(Action>> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), "market.tickers"); - var internalHandler = new Action>>>(data => - { - var result = new HuobiSymbolDatas { Timestamp = data.Timestamp, Ticks = data.Data.Data }; - onData(data.As(result)); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription>(_logger, $"market.tickers", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToTickerUpdatesAsync(string symbol, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.ticker"); - var internalHandler = new Action>>(data => - { - data.Data.Data.Symbol = symbol; - onData(data.As(data.Data.Data)); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.ticker", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToBestOfferUpdatesAsync(string symbol, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{symbol}.bbo"); - var internalHandler = new Action>>(data => - { - onData(data.As(data.Data.Data, symbol)); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{symbol}.bbo", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws"), subscription, ct).ConfigureAwait(false); } /// @@ -243,43 +245,9 @@ public async Task> SubscribeToOrderUpdatesAsync( CancellationToken ct = default) { symbol = symbol?.ValidateHuobiSymbol(); - var request = new HuobiAuthenticatedSubscribeRequest($"orders#{symbol ?? "*"}"); - var internalHandler = new Action>(data => - { - if (data.Data["data"] == null || data.Data["data"]!["eventType"] == null) - { - _logger.Log(LogLevel.Warning, "Invalid order update data: " + data); - return; - } - - var eventType = data.Data["data"]!["eventType"]?.ToString(); - var symbol = data.Data["data"]!["symbol"]?.ToString(); - if (eventType == "trigger") - { - DeserializeAndInvoke(data, onConditionalOrderTriggerFailure, symbol); - } - else if (eventType == "deletion") - { - DeserializeAndInvoke(data, onConditionalOrderCanceled, symbol); - } - else if (eventType == "creation") - { - DeserializeAndInvoke(data, onOrderSubmitted, symbol); - } - else if (eventType == "trade") - { - DeserializeAndInvoke(data, onOrderMatched, symbol); - } - else if (eventType == "cancellation") - { - DeserializeAndInvoke(data, onOrderCancelation, symbol); - } - else - { - _logger.Log(LogLevel.Warning, "Unknown order event type: " + eventType); - } - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/v2"), request, null, true, internalHandler, ct).ConfigureAwait(false); + + var subscription = new HuobiOrderSubscription(_logger, symbol, onOrderSubmitted, onOrderMatched, onOrderCancelation, onConditionalOrderTriggerFailure, onConditionalOrderCanceled); + return await SubscribeAsync(BaseAddress.AppendPath("ws/v2"), subscription, ct).ConfigureAwait(false); } /// @@ -288,469 +256,15 @@ public async Task> SubscribeToAccountUpdatesAsync if (updateMode != null && (updateMode > 2 || updateMode < 0)) throw new ArgumentException("UpdateMode should be either 0, 1 or 2"); - var request = new HuobiAuthenticatedSubscribeRequest("accounts.update#" + (updateMode ?? 1)); - var internalHandler = new Action>(data => - { - DeserializeAndInvoke(data, onAccountUpdate); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/v2"), request, null, true, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiAccountSubscription(_logger, "accounts.update#" + (updateMode ?? 1), onAccountUpdate, true); + return await SubscribeAsync(BaseAddress.AppendPath("ws/v2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToOrderDetailsUpdatesAsync(string? symbol = null, Action>? onOrderMatch = null, Action>? onOrderCancel = null, CancellationToken ct = default) { - var request = new HuobiAuthenticatedSubscribeRequest($"trade.clearing#{symbol ?? "*"}#1"); - var internalHandler = new Action>(data => - { - if (data.Data["data"] == null || data.Data["data"]!["eventType"] == null) - { - _logger.Log(LogLevel.Warning, "Invalid order update data: " + data); - return; - } - - var eventType = data.Data["data"]!["eventType"]?.ToString(); - var symbol = data.Data["data"]!["symbol"]?.ToString(); - if (eventType == "trade") - { - DeserializeAndInvoke(data, onOrderMatch, symbol); - } - else if (eventType == "cancellation") - { - DeserializeAndInvoke(data, onOrderCancel, symbol); - } - else - { - _logger.Log(LogLevel.Warning, "Unknown order details event type: " + eventType); - } - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/v2"), request, null, true, internalHandler, ct).ConfigureAwait(false); - } - - #region private - - private void DeserializeAndInvoke(DataEvent data, Action>? action, string? symbol = null) - { - var obj = Deserialize(data.Data["data"]!); - if (!obj) - { - _logger.Log(LogLevel.Error, $"Failed to deserialize {typeof(T).Name}: " + obj.Error); - return; - } - action?.Invoke(data.As(obj.Data, symbol)); - } - - - private void PingHandlerV1(MessageEvent messageEvent) - { - var v1Ping = messageEvent.JsonData["ping"] != null; - - if (v1Ping) - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new HuobiPingResponse(messageEvent.JsonData["ping"]!.Value()), 1); - } - - private void PingHandlerV2(MessageEvent messageEvent) - { - var v2Ping = messageEvent.JsonData["action"]?.ToString() == "ping"; - - if (v2Ping) - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new HuobiPingAuthResponse(messageEvent.JsonData["data"]!["ts"]!.Value()), 1); - } - - private void PingHandlerV3(MessageEvent messageEvent) - { - var v3Ping = messageEvent.JsonData["op"]?.ToString() == "ping"; - - if (v3Ping) - { - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new - { - op = "pong", - ts = messageEvent.JsonData["ts"]?.ToString() - }, 1); - } - } - - private static string DecompressData(byte[] byteData) - { - using var decompressedStream = new MemoryStream(); - using var compressedStream = new MemoryStream(byteData); - using var deflateStream = new GZipStream(compressedStream, CompressionMode.Decompress); - deflateStream.CopyTo(decompressedStream); - decompressedStream.Position = 0; - - using var streamReader = new StreamReader(decompressedStream); - return streamReader.ReadToEnd(); - } - /// - protected override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) - { - callResult = new CallResult(default(T)!); - var v1Data = (data["data"] != null || data["tick"] != null) && data["rep"] != null; - var v1Error = data["status"] != null && data["status"]!.ToString() == "error"; - var isV1QueryResponse = v1Data || v1Error; - if (isV1QueryResponse) - { - var hRequest = (HuobiSocketRequest)request; - var id = data["id"]; - if (id == null) - return false; - - if (id.ToString() != hRequest.Id) - return false; - - if (v1Error) - { - var error = new ServerError(data["err-msg"]!.ToString()); - callResult = new CallResult(error); - return true; - } - - var desResult = Deserialize(data); - if (!desResult) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Failed to deserialize data: {desResult.Error}. Data: {data}"); - callResult = new CallResult(desResult.Error!); - return true; - } - - callResult = new CallResult(desResult.Data); - return true; - } - - var action = data["action"]?.ToString(); - var isV2Response = action == "req"; - if (isV2Response) - { - var hRequest = (HuobiAuthenticatedSubscribeRequest)request; - var channel = data["ch"]?.ToString(); - if (channel != hRequest.Channel) - return false; - - var desResult = Deserialize(data); - if (!desResult) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Failed to deserialize data: {desResult.Error}. Data: {data}"); - return false; - } - - callResult = new CallResult(desResult.Data); - return true; - } - - return false; - } - - /// - protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult? callResult) - { - callResult = null; - var status = message["status"]?.ToString(); - var isError = status == "error"; - if (isError) - { - if (request is HuobiSubscribeRequest hRequest) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - return false; - } - - var id = subResponse.Data.Id; - if (id != hRequest.Id) - return false; // Not for this request - - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.ErrorMessage); - callResult = new CallResult(new ServerError($"{subResponse.Data.ErrorCode}, {subResponse.Data.ErrorMessage}")); - return true; - } - - if (request is HuobiAuthenticatedSubscribeRequest haRequest) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - callResult = new CallResult(subResponse.Error!); - return false; - } - - var id = subResponse.Data.Channel; - if (id != haRequest.Channel) - return false; // Not for this request - - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.Code); - callResult = new CallResult(new ServerError(subResponse.Data.Code, "Failed to subscribe")); - return true; - } - } - - var v1Sub = message["subbed"] != null; - if (v1Sub) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - return false; - } - - var hRequest = (HuobiSubscribeRequest)request; - if (subResponse.Data.Id != hRequest.Id) - return false; - - if (!subResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.ErrorMessage); - callResult = new CallResult(new ServerError($"{subResponse.Data.ErrorCode}, {subResponse.Data.ErrorMessage}")); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Subscription completed"); - callResult = new CallResult(subResponse.Data); - return true; - } - - var action = message["action"]?.ToString(); - var v2Sub = action == "sub"; - if (v2Sub) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - callResult = new CallResult(subResponse.Error!); - return false; - } - - var hRequest = (HuobiAuthenticatedSubscribeRequest)request; - if (subResponse.Data.Channel != hRequest.Channel) - return false; - - if (!subResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.Message); - callResult = new CallResult(new ServerError(subResponse.Data.Code, subResponse.Data.Message)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Subscription completed"); - callResult = new CallResult(subResponse.Data); - return true; - } - - var operation = message["op"]?.ToString(); - var usdtMarginSub = operation == "sub"; - if (usdtMarginSub) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - callResult = new CallResult(subResponse.Error!); - return false; - } - - var hRequest = (HuobiSocketRequest2)request; - if (subResponse.Data.Topic != hRequest.Topic) - return false; - - if (!subResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.ErrorMessage); - callResult = new CallResult(new ServerError(subResponse.Data.ErrorCode + " - " + subResponse.Data.ErrorMessage)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Subscription completed"); - callResult = new CallResult(subResponse.Data); - return true; - } - - return false; - } - - /// - protected override bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request) - { - if (request is HuobiSubscribeRequest hRequest) - return hRequest.Topic == message["ch"]?.ToString(); - - if (request is HuobiAuthenticatedSubscribeRequest haRequest) - return haRequest.Channel == message["ch"]?.ToString(); - - if (request is HuobiSocketRequest2 hRequest2) - { - if (hRequest2.Topic == message["topic"]?.ToString()) - return true; - - if (hRequest2.Topic.Contains("*") && hRequest2.Topic.Split('.')[0] == message["topic"]?.ToString().Split('.')[0]) - return true; - } - - return false; - } - - /// - protected override bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier) - { - if (message.Type != JTokenType.Object) - return false; - - if (identifier == "PingV1" && message["ping"] != null) - return true; - - if (identifier == "PingV2" && message["action"]?.ToString() == "ping") - return true; - - if (identifier == "PingV3" && message["op"]?.ToString() == "ping") - return true; - - return false; - } - - /// - protected override async Task> AuthenticateSocketAsync(SocketConnection s) - { - if (s.ApiClient.AuthenticationProvider == null) - return new CallResult(new NoApiCredentialsError()); - - var result = new CallResult(new ServerError("No response from server")); - if (s.ApiClient is HuobiSocketClientUsdtMarginSwapApi) - { - await s.SendAndWaitAsync(((HuobiAuthenticationProvider)s.ApiClient.AuthenticationProvider).GetWebsocketAuthentication2(s.ConnectionUri), ClientOptions.RequestTimeout, null, 1, data => - { - if (data["op"]?.ToString() != "auth") - return false; - - var authResponse = Deserialize(data); - if (!authResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Authorization failed: " + authResponse.Error); - result = new CallResult(authResponse.Error!); - return true; - } - if (!authResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Authorization failed: " + authResponse.Data.Message); - result = new CallResult(new ServerError(authResponse.Data.Code, authResponse.Data.Message)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Authorization completed"); - result = new CallResult(true); - return true; - }).ConfigureAwait(false); - } - else - { - await s.SendAndWaitAsync(((HuobiAuthenticationProvider)s.ApiClient.AuthenticationProvider).GetWebsocketAuthentication(s.ConnectionUri), ClientOptions.RequestTimeout, null, 1, data => - { - if (data["ch"]?.ToString() != "auth") - return false; - - var authResponse = Deserialize(data); - if (!authResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Authorization failed: " + authResponse.Error); - result = new CallResult(authResponse.Error!); - return true; - } - if (!authResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Authorization failed: " + authResponse.Data.Message); - result = new CallResult(new ServerError(authResponse.Data.Code, authResponse.Data.Message)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Authorization completed"); - result = new CallResult(true); - return true; - }).ConfigureAwait(false); - } - - return result; - } - - /// - protected override async Task UnsubscribeAsync(SocketConnection connection, SocketSubscription s) - { - var result = false; - if (s.Request is HuobiSubscribeRequest hRequest) - { - var unsubId = ExchangeHelpers.NextId().ToString(); - var unsub = new HuobiUnsubscribeRequest(unsubId, hRequest.Topic); - - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - var id = data["id"]?.ToString(); - if (id == unsubId) - { - result = data["status"]?.ToString() == "ok"; - return true; - } - - return false; - }).ConfigureAwait(false); - return result; - } - - if (s.Request is HuobiAuthenticatedSubscribeRequest haRequest) - { - var unsub = new Dictionary() - { - { "action", "unsub" }, - { "ch", haRequest.Channel }, - }; - - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - if (data["action"]?.ToString() == "unsub" && data["ch"]?.ToString() == haRequest.Channel) - { - result = data["code"]?.Value() == 200; - return true; - } - - return false; - }).ConfigureAwait(false); - return result; - } - - if (s.Request is HuobiSocketRequest2 hRequest2) - { - var unsub = new - { - op = "unsub", - topic = hRequest2.Topic, - cid = ExchangeHelpers.NextId().ToString() - }; - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - if (data["op"]?.ToString() == "unsub" && data["cid"]?.ToString() == unsub.cid) - { - result = data["err-code"]?.Value() == 0; - return true; - } - - return false; - }).ConfigureAwait(false); - return result; - } - - throw new InvalidOperationException("Unknown request type"); + var subscription = new HuobiOrderDetailsSubscription(_logger, symbol, onOrderMatch, onOrderCancel); + return await SubscribeAsync(BaseAddress.AppendPath("ws/v2"), subscription, ct).ConfigureAwait(false); } - #endregion - #endregion - } } diff --git a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApi.cs b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApi.cs index a292bad7..1821521e 100644 --- a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApi.cs +++ b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApi.cs @@ -4,7 +4,6 @@ using CryptoExchange.Net.Objects; using Huobi.Net.Clients.UsdtMarginSwapApi; using Huobi.Net.Interfaces.Clients.UsdtMarginSwapApi; -using Huobi.Net.Objects; using Huobi.Net.Objects.Internal; using Huobi.Net.Objects.Options; using Microsoft.Extensions.Logging; @@ -12,7 +11,6 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiAccount.cs b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiAccount.cs index 850fa5f4..0c71b97d 100644 --- a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiAccount.cs +++ b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiAccount.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using System.Threading; using CryptoExchange.Net.Converters; diff --git a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiExchangeData.cs b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiExchangeData.cs index f5714bdb..a57eeb2b 100644 --- a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiExchangeData.cs +++ b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiExchangeData.cs @@ -6,12 +6,10 @@ using Huobi.Net.Interfaces.Clients.UsdtMarginSwapApi; using Huobi.Net.Objects.Models; using Huobi.Net.Objects.Models.UsdtMarginSwap; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiTrading.cs b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiTrading.cs index a3652fc7..e0bdb03b 100644 --- a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiTrading.cs +++ b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiClientUsdtMarginSwapApiTrading.cs @@ -5,11 +5,9 @@ using Huobi.Net.Enums; using Huobi.Net.Interfaces.Clients.UsdtMarginSwapApi; using Huobi.Net.Objects.Models.UsdtMarginSwap; -using System; using System.Collections.Generic; using System.Globalization; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiSocketClientUsdtMarginSwapApi.cs b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiSocketClientUsdtMarginSwapApi.cs index 361fcf08..48815754 100644 --- a/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiSocketClientUsdtMarginSwapApi.cs +++ b/Huobi.Net/Clients/UsdtMarginSwapApi/HuobiSocketClientUsdtMarginSwapApi.cs @@ -1,31 +1,34 @@ using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; using System.IO.Compression; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets.MessageParsing; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Huobi.Net.Converters; using Huobi.Net.Enums; using Huobi.Net.Interfaces.Clients.UsdtMarginSwapApi; -using Huobi.Net.Objects; -using Huobi.Net.Objects.Internal; using Huobi.Net.Objects.Models; using Huobi.Net.Objects.Models.Socket; using Huobi.Net.Objects.Options; +using Huobi.Net.Objects.Sockets.Subscriptions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Huobi.Net.Clients.SpotApi { /// public class HuobiSocketClientUsdtMarginSwapApi : SocketApiClient, IHuobiSocketClientUsdtMarginSwapApi { + private static readonly MessagePath _idPath = MessagePath.Get().Property("id"); + private static readonly MessagePath _actionPath = MessagePath.Get().Property("action"); + private static readonly MessagePath _channelPath = MessagePath.Get().Property("ch"); + private static readonly MessagePath _pingPath = MessagePath.Get().Property("ping"); #region ctor internal HuobiSocketClientUsdtMarginSwapApi(ILogger logger, HuobiSocketOptions options) @@ -33,557 +36,169 @@ internal HuobiSocketClientUsdtMarginSwapApi(ILogger logger, HuobiSocketOptions o { KeepAliveInterval = TimeSpan.Zero; - SetDataInterpreter(DecompressData, null); - AddGenericHandler("PingV1", PingHandlerV1); - AddGenericHandler("PingV2", PingHandlerV2); - AddGenericHandler("PingV3", PingHandlerV3); + AddSystemSubscription(new HuobiPingSubscription(_logger)); } #endregion + /// + public override Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) + { + if (type != WebSocketMessageType.Binary) + return stream; + + var decompressedStream = new MemoryStream(); + using var deflateStream = new GZipStream(stream, CompressionMode.Decompress); + deflateStream.CopyTo(decompressedStream); + decompressedStream.Position = 0; + return decompressedStream; + } + + /// + public override string? GetListenerIdentifier(IMessageAccessor message) + { + var id = message.GetValue(_idPath); + if (id != null) + return id; + + var ping = message.GetValue(_pingPath); + if (ping != null) + return "pingV3"; + + var channel = message.GetValue(_channelPath); + var action = message.GetValue(_actionPath); + if (action != null && action != "push") + return action + channel; + + return channel; + } + /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => new HuobiAuthenticationProvider(credentials, false); - #region methods - /// public async Task> SubscribeToKlineUpdatesAsync(string contractCode, KlineInterval period, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{contractCode}.kline.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.kline.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToOrderBookUpdatesAsync(string contractCode, int mergeStep, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), $"market.{contractCode}.depth.step" + mergeStep); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.depth.step" + mergeStep, onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToIncrementalOrderBookUpdatesAsync(string contractCode, bool snapshot, int limit, Action> onData, CancellationToken ct = default) { - var request = new HuobiIncrementalOrderBookSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.depth.size_{limit}.high_freq", - snapshot ? "snapshot" : "incremental"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.depth.size_{limit}.high_freq", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToSymbolTickerUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.detail"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.detail", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToBestOfferUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.bbo"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.bbo", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToTradeUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.trade.detail"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.trade.detail", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("linear-swap-ws"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToIndexKlineUpdatesAsync(string contractCode, KlineInterval period, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.index.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.index.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToPremiumIndexKlineUpdatesAsync(string contractCode, KlineInterval period, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.premium_index.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.premium_index.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToEstimatedFundingRateKlineUpdatesAsync(string contractCode, KlineInterval period, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.estimated_rate.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.estimated_rate.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToBasisUpdatesAsync(string contractCode, KlineInterval period, string priceType, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.basis.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}.{priceType}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), request, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.basis.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}.{priceType}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToMarkPriceKlineUpdatesAsync(string contractCode, KlineInterval period, Action> onData, CancellationToken ct = default) { - var request = new HuobiSubscribeRequest( - ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), - $"market.{contractCode}.mark_price.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}"); - var internalHandler = new Action>>(data => onData(data.As(data.Data.Data, contractCode))); - return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), request, null, false, internalHandler, ct).ConfigureAwait(false); - } - - // WIP - - ///// - //public async Task> SubscribeToIsolatedMarginOrderUpdatesAsync(Action> onData, CancellationToken ct = default) - //{ - // var request = new HuobiSocketRequest2( - // "sub", - // NextId().ToString(CultureInfo.InvariantCulture), - // $"orders.*"); - // var internalHandler = new Action>(data => onData(data.As(data.Data, data.Data.ContractCode))); - // return await SubscribeAsync( _baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); - //} - - ///// - //public async Task> SubscribeToIsolatedMarginOrderUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) - //{ - // var request = new HuobiSocketRequest2( - // "sub", - // NextId().ToString(CultureInfo.InvariantCulture), - // $"orders.{contractCode}"); - // var internalHandler = new Action>(data => onData(data.As(data.Data, contractCode))); - // return await SubscribeAsync( _baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); - //} - - ///// - //public async Task> SubscribeToCrossMarginOrderUpdatesAsync(Action> onData, CancellationToken ct = default) - //{ - // var request = new HuobiSocketRequest2( - // "sub", - // NextId().ToString(CultureInfo.InvariantCulture), - // $"orders_cross.*"); - // var internalHandler = new Action>(data => onData(data.As(data.Data, data.Data.ContractCode))); - // return await SubscribeAsync(_baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); - //} - - ///// - //public async Task> SubscribeToCrossMarginOrderUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) - //{ - // var request = new HuobiSocketRequest2( - // "sub", - // NextId().ToString(CultureInfo.InvariantCulture), - // $"orders_cross.{contractCode}"); - // var internalHandler = new Action>(data => onData(data.As(data.Data, contractCode))); - // return await SubscribeAsync(_baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); - //} - - #region private - - private void PingHandlerV1(MessageEvent messageEvent) - { - var v1Ping = messageEvent.JsonData["ping"] != null; - - if (v1Ping) - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new HuobiPingResponse(messageEvent.JsonData["ping"]!.Value()), 1); - } - - private void PingHandlerV2(MessageEvent messageEvent) - { - var v2Ping = messageEvent.JsonData["action"]?.ToString() == "ping"; - - if (v2Ping) - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new HuobiPingAuthResponse(messageEvent.JsonData["data"]!["ts"]!.Value()), 1); - } - - private void PingHandlerV3(MessageEvent messageEvent) - { - var v3Ping = messageEvent.JsonData["op"]?.ToString() == "ping"; - - if (v3Ping) - { - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new - { - op = "pong", - ts = messageEvent.JsonData["ts"]?.ToString() - }, 1); - } - } - - private static string DecompressData(byte[] byteData) - { - using var decompressedStream = new MemoryStream(); - using var compressedStream = new MemoryStream(byteData); - using var deflateStream = new GZipStream(compressedStream, CompressionMode.Decompress); - deflateStream.CopyTo(decompressedStream); - decompressedStream.Position = 0; - - using var streamReader = new StreamReader(decompressedStream); - return streamReader.ReadToEnd(); - } - /// - protected override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) - { - callResult = new CallResult(default(T)!); - var v1Data = (data["data"] != null || data["tick"] != null) && data["rep"] != null; - var v1Error = data["status"] != null && data["status"]!.ToString() == "error"; - var isV1QueryResponse = v1Data || v1Error; - if (isV1QueryResponse) - { - var hRequest = (HuobiSocketRequest)request; - var id = data["id"]; - if (id == null) - return false; - - if (id.ToString() != hRequest.Id) - return false; - - if (v1Error) - { - var error = new ServerError(data["err-msg"]!.ToString()); - callResult = new CallResult(error); - return true; - } - - var desResult = Deserialize(data); - if (!desResult) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Failed to deserialize data: {desResult.Error}. Data: {data}"); - callResult = new CallResult(desResult.Error!); - return true; - } - - callResult = new CallResult(desResult.Data); - return true; - } - - var action = data["action"]?.ToString(); - var isV2Response = action == "req"; - if (isV2Response) - { - var hRequest = (HuobiAuthenticatedSubscribeRequest)request; - var channel = data["ch"]?.ToString(); - if (channel != hRequest.Channel) - return false; - - var desResult = Deserialize(data); - if (!desResult) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Failed to deserialize data: {desResult.Error}. Data: {data}"); - return false; - } - - callResult = new CallResult(desResult.Data); - return true; - } - - return false; - } - - /// - protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult? callResult) - { - callResult = null; - var status = message["status"]?.ToString(); - var isError = status == "error"; - if (isError) - { - if (request is HuobiSubscribeRequest hRequest) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - return false; - } - - var id = subResponse.Data.Id; - if (id != hRequest.Id) - return false; // Not for this request - - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.ErrorMessage); - callResult = new CallResult(new ServerError($"{subResponse.Data.ErrorCode}, {subResponse.Data.ErrorMessage}")); - return true; - } - - if (request is HuobiAuthenticatedSubscribeRequest haRequest) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - callResult = new CallResult(subResponse.Error!); - return false; - } - - var id = subResponse.Data.Channel; - if (id != haRequest.Channel) - return false; // Not for this request - - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.Code); - callResult = new CallResult(new ServerError(subResponse.Data.Code, "Failed to subscribe")); - return true; - } - } - - var v1Sub = message["subbed"] != null; - if (v1Sub) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - return false; - } - - var hRequest = (HuobiSubscribeRequest)request; - if (subResponse.Data.Id != hRequest.Id) - return false; - - if (!subResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.ErrorMessage); - callResult = new CallResult(new ServerError($"{subResponse.Data.ErrorCode}, {subResponse.Data.ErrorMessage}")); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Subscription completed"); - callResult = new CallResult(subResponse.Data); - return true; - } - - var action = message["action"]?.ToString(); - var v2Sub = action == "sub"; - if (v2Sub) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - callResult = new CallResult(subResponse.Error!); - return false; - } - - var hRequest = (HuobiAuthenticatedSubscribeRequest)request; - if (subResponse.Data.Channel != hRequest.Channel) - return false; - - if (!subResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.Message); - callResult = new CallResult(new ServerError(subResponse.Data.Code, subResponse.Data.Message)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Subscription completed"); - callResult = new CallResult(subResponse.Data); - return true; - } - - var operation = message["op"]?.ToString(); - var usdtMarginSub = operation == "sub"; - if (usdtMarginSub) - { - var subResponse = Deserialize(message); - if (!subResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Error); - callResult = new CallResult(subResponse.Error!); - return false; - } - - var hRequest = (HuobiSocketRequest2)request; - if (subResponse.Data.Topic != hRequest.Topic) - return false; - - if (!subResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Subscription failed: " + subResponse.Data.ErrorMessage); - callResult = new CallResult(new ServerError(subResponse.Data.ErrorCode + " - " + subResponse.Data.ErrorMessage)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Subscription completed"); - callResult = new CallResult(subResponse.Data); - return true; - } - - return false; - } - - /// - protected override bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request) - { - if (request is HuobiSubscribeRequest hRequest) - return hRequest.Topic == message["ch"]?.ToString(); - - if (request is HuobiAuthenticatedSubscribeRequest haRequest) - return haRequest.Channel == message["ch"]?.ToString(); - - if (request is HuobiSocketRequest2 hRequest2) - { - if (hRequest2.Topic == message["topic"]?.ToString()) - return true; - - if (hRequest2.Topic.Contains("*") && hRequest2.Topic.Split('.')[0] == message["topic"]?.ToString().Split('.')[0]) - return true; - } - - return false; - } - - /// - protected override bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier) - { - if (message.Type != JTokenType.Object) - return false; - - if (identifier == "PingV1" && message["ping"] != null) - return true; - - if (identifier == "PingV2" && message["action"]?.ToString() == "ping") - return true; - - if (identifier == "PingV3" && message["op"]?.ToString() == "ping") - return true; - - return false; - } - - /// - protected override async Task> AuthenticateSocketAsync(SocketConnection s) - { - if (s.ApiClient.AuthenticationProvider == null) - return new CallResult(new NoApiCredentialsError()); - - var result = new CallResult(new ServerError("No response from server")); - await s.SendAndWaitAsync(((HuobiAuthenticationProvider)s.ApiClient.AuthenticationProvider).GetWebsocketAuthentication2(s.ConnectionUri), ClientOptions.RequestTimeout, null, 1, data => - { - if (data["op"]?.ToString() != "auth") - return false; - - var authResponse = Deserialize(data); - if (!authResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Authorization failed: " + authResponse.Error); - result = new CallResult(authResponse.Error!); - return true; - } - if (!authResponse.Data.IsSuccessful) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} Authorization failed: " + authResponse.Data.Message); - result = new CallResult(new ServerError(authResponse.Data.Code, authResponse.Data.Message)); - return true; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} Authorization completed"); - result = new CallResult(true); - return true; - }).ConfigureAwait(false); - - return result; - } - - /// - protected override async Task UnsubscribeAsync(SocketConnection connection, SocketSubscription s) - { - var result = false; - if (s.Request is HuobiSubscribeRequest hRequest) - { - var unsubId = ExchangeHelpers.NextId().ToString(); - var unsub = new HuobiUnsubscribeRequest(unsubId, hRequest.Topic); - - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - var id = data["id"]?.ToString(); - if (id == unsubId) - { - result = data["status"]?.ToString() == "ok"; - return true; - } - - return false; - }).ConfigureAwait(false); - return result; - } - - if (s.Request is HuobiAuthenticatedSubscribeRequest haRequest) - { - var unsub = new Dictionary() - { - { "action", "unsub" }, - { "ch", haRequest.Channel }, - }; - - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - if (data["action"]?.ToString() == "unsub" && data["ch"]?.ToString() == haRequest.Channel) - { - result = data["code"]?.Value() == 200; - return true; - } - - return false; - }).ConfigureAwait(false); - return result; - } - - if (s.Request is HuobiSocketRequest2 hRequest2) - { - var unsub = new - { - op = "unsub", - topic = hRequest2.Topic, - cid = ExchangeHelpers.NextId().ToString() - }; - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - if (data["op"]?.ToString() == "unsub" && data["cid"]?.ToString() == unsub.cid) - { - result = data["err-code"]?.Value() == 0; - return true; - } - - return false; - }).ConfigureAwait(false); - return result; - } - - throw new InvalidOperationException("Unknown request type"); - } - #endregion - #endregion + var subscription = new HuobiSubscription(_logger, $"market.{contractCode.ToUpperInvariant()}.mark_price.{JsonConvert.SerializeObject(period, new PeriodConverter(false))}", onData, false); + return await SubscribeAsync(BaseAddress.AppendPath("ws_index"), subscription, ct).ConfigureAwait(false); + } + + //// WIP + + /////// + ////public async Task> SubscribeToIsolatedMarginOrderUpdatesAsync(Action> onData, CancellationToken ct = default) + ////{ + //// var request = new HuobiSocketRequest2( + //// "sub", + //// NextId().ToString(CultureInfo.InvariantCulture), + //// $"orders.*"); + //// var internalHandler = new Action>(data => onData(data.As(data.Data, data.Data.ContractCode))); + //// return await SubscribeAsync( _baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); + ////} + + /////// + ////public async Task> SubscribeToIsolatedMarginOrderUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) + ////{ + //// var request = new HuobiSocketRequest2( + //// "sub", + //// NextId().ToString(CultureInfo.InvariantCulture), + //// $"orders.{contractCode}"); + //// var internalHandler = new Action>(data => onData(data.As(data.Data, contractCode))); + //// return await SubscribeAsync( _baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); + ////} + + /////// + ////public async Task> SubscribeToCrossMarginOrderUpdatesAsync(Action> onData, CancellationToken ct = default) + ////{ + //// var request = new HuobiSocketRequest2( + //// "sub", + //// NextId().ToString(CultureInfo.InvariantCulture), + //// $"orders_cross.*"); + //// var internalHandler = new Action>(data => onData(data.As(data.Data, data.Data.ContractCode))); + //// return await SubscribeAsync(_baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); + ////} + + /////// + ////public async Task> SubscribeToCrossMarginOrderUpdatesAsync(string contractCode, Action> onData, CancellationToken ct = default) + ////{ + //// var request = new HuobiSocketRequest2( + //// "sub", + //// NextId().ToString(CultureInfo.InvariantCulture), + //// $"orders_cross.{contractCode}"); + //// var internalHandler = new Action>(data => onData(data.As(data.Data, contractCode))); + //// return await SubscribeAsync(_baseAddressAuthenticated, request, null, true, internalHandler, ct).ConfigureAwait(false); + ////} } } diff --git a/Huobi.Net/ExtensionMethods/CryptoClientExtensions.cs b/Huobi.Net/ExtensionMethods/CryptoClientExtensions.cs new file mode 100644 index 00000000..ee966089 --- /dev/null +++ b/Huobi.Net/ExtensionMethods/CryptoClientExtensions.cs @@ -0,0 +1,25 @@ +using Huobi.Net.Clients; +using Huobi.Net.Interfaces.Clients; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Extensions for the ICryptoRestClient and ICryptoSocketClient interfaces + /// + public static class CryptoClientExtensions + { + /// + /// Get the Huobi REST Api client + /// + /// + /// + public static IHuobiRestClient Huobi(this ICryptoRestClient baseClient) => baseClient.TryGet(() => new HuobiRestClient()); + + /// + /// Get the Huobi Websocket Api client + /// + /// + /// + public static IHuobiSocketClient Huobi(this ICryptoSocketClient baseClient) => baseClient.TryGet(() => new HuobiSocketClient()); + } +} diff --git a/Huobi.Net/ExtensionMethods/HuobiExtensionMethods.cs b/Huobi.Net/ExtensionMethods/HuobiExtensionMethods.cs new file mode 100644 index 00000000..9be0cd2b --- /dev/null +++ b/Huobi.Net/ExtensionMethods/HuobiExtensionMethods.cs @@ -0,0 +1,26 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Huobi.Net.ExtensionMethods +{ + /// + /// Extension methods specific to using the Huobi API + /// + public static class HuobiExtensionMethods + { + /// + /// Validate the string is a valid Huobi symbol. + /// + /// string to validate + public static string ValidateHuobiSymbol(this string symbolString) + { + if (string.IsNullOrEmpty(symbolString)) + throw new ArgumentException("Symbol is not provided"); + symbolString = symbolString.ToLower(CultureInfo.InvariantCulture); + if (!Regex.IsMatch(symbolString, "^(([a-z]|[0-9]){4,})$")) + throw new ArgumentException($"{symbolString} is not a valid Huobi symbol. Should be [QuoteAsset][BaseAsset], e.g. ETHBTC"); + return symbolString; + } + } +} diff --git a/Huobi.Net/HuobiHelpers.cs b/Huobi.Net/ExtensionMethods/ServiceCollectionExtensions.cs similarity index 73% rename from Huobi.Net/HuobiHelpers.cs rename to Huobi.Net/ExtensionMethods/ServiceCollectionExtensions.cs index a48f7324..b0e3ca06 100644 --- a/Huobi.Net/HuobiHelpers.cs +++ b/Huobi.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -1,21 +1,20 @@ -using Huobi.Net.Clients; +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Interfaces; +using Huobi.Net.Clients; +using Huobi.Net.Interfaces; using Huobi.Net.Interfaces.Clients; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Globalization; -using System.Net.Http; -using System.Net; -using System.Text.RegularExpressions; using Huobi.Net.Objects.Options; -using Huobi.Net.Interfaces; using Huobi.Net.SymbolOrderBooks; +using System; +using System.Net; +using System.Net.Http; -namespace Huobi.Net +namespace Microsoft.Extensions.DependencyInjection { /// - /// Helpers for Huobi + /// Extensions for DI /// - public static class HuobiHelpers + public static class ServiceCollectionExtensions { /// /// Add the IHuobiClient and IHuobiSocketClient to the sevice collection so they can be injected @@ -58,8 +57,9 @@ public static IServiceCollection AddHuobi( return handler; }); + services.AddTransient(); + services.AddTransient(); services.AddSingleton(); - services.AddTransient(); services.AddTransient(x => x.GetRequiredService().SpotApi.CommonSpotClient); if (socketClientLifeTime == null) services.AddSingleton(); @@ -67,19 +67,5 @@ public static IServiceCollection AddHuobi( services.Add(new ServiceDescriptor(typeof(IHuobiSocketClient), typeof(HuobiSocketClient), socketClientLifeTime.Value)); return services; } - - /// - /// Validate the string is a valid Huobi symbol. - /// - /// string to validate - public static string ValidateHuobiSymbol(this string symbolString) - { - if (string.IsNullOrEmpty(symbolString)) - throw new ArgumentException("Symbol is not provided"); - symbolString = symbolString.ToLower(CultureInfo.InvariantCulture); - if (!Regex.IsMatch(symbolString, "^(([a-z]|[0-9]){4,})$")) - throw new ArgumentException($"{symbolString} is not a valid Huobi symbol. Should be [QuoteAsset][BaseAsset], e.g. ETHBTC"); - return symbolString; - } } } diff --git a/Huobi.Net/Huobi.Net.csproj b/Huobi.Net/Huobi.Net.csproj index c1eb57f0..91d9f562 100644 --- a/Huobi.Net/Huobi.Net.csproj +++ b/Huobi.Net/Huobi.Net.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netstandard2.1 enable @@ -7,9 +7,9 @@ Huobi.Net JKorf - 5.0.5 - 5.0.5 - 5.0.5 + 5.1.0-beta2 + 5.1.0 + 5.1.0 Huobi.Net is a .Net wrapper for the Huobi API. It includes all features the API provides, REST API and Websocket, using clear and readable objects including but not limited to Reading market info, Placing and managing orders and Reading balances and funds false Huobi Huobi.Net C# .Net CryptoCurrency Exchange API wrapper @@ -21,7 +21,7 @@ README.md en true - 5.0.5 - Updated CryptoExchange.Net + 5.1.0-beta2 - Fixed ping responses websockets, Fixed lower case symbol usdt margin subscription handling Huobi.Net.xml @@ -48,10 +48,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - \ No newline at end of file diff --git a/Huobi.Net/Huobi.Net.xml b/Huobi.Net/Huobi.Net.xml index 42185a01..c1599c23 100644 --- a/Huobi.Net/Huobi.Net.xml +++ b/Huobi.Net/Huobi.Net.xml @@ -19,11 +19,6 @@ Option configuration delegate - - - Create a new instance of the HuobiRestClient using default options - - Create a new instance of the HuobiRestClient @@ -339,133 +334,112 @@ - + - + - - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + @@ -2228,6 +2202,17 @@ Withdraw + + + Extension methods specific to using the Huobi API + + + + + Validate the string is a valid Huobi symbol. + + string to validate + Huobi environments @@ -2269,27 +2254,6 @@ - - - Helpers for Huobi - - - - - Add the IHuobiClient and IHuobiSocketClient to the sevice collection so they can be injected - - The service collection - Set default options for the rest client - Set default options for the socket client - The lifetime of the IHuobiSocketClient for the service collection. Defaults to Singleton. - - - - - Validate the string is a valid Huobi symbol. - - string to validate - Client for accessing the Huobi API. @@ -3061,7 +3025,7 @@ The period of a single candlestick - + Subscribes to candlestick updates for a symbol @@ -3089,7 +3053,7 @@ The amount of rows. 5, 20, 150 or 400 - + Subscribes to order book updates for a symbol @@ -3100,7 +3064,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribes to order book updates for a symbol @@ -3111,7 +3075,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribes to order book updates for a symbol, @@ -3130,7 +3094,7 @@ The symbol to get trades for - + Subscribes to trade updates for a symbol @@ -3148,7 +3112,7 @@ The symbol to get data for - + Subscribes to symbol detail updates for a symbol @@ -3158,7 +3122,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribes to updates for a symbol @@ -3168,7 +3132,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribes to updates for all tickers @@ -3177,7 +3141,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribe to changes of a symbol's best ask/bid @@ -3187,7 +3151,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribe to updates of orders @@ -3201,7 +3165,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribe to updates of account balances @@ -3211,7 +3175,7 @@ Cancellation token for closing this subscription A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - + Subscribe to detailed order matched/canceled updates @@ -4111,7 +4075,7 @@ Usdt margin swap streams - + Subscribe to basis updates @@ -4123,7 +4087,7 @@ Cancellation token - + Subscribe to best offer updates @@ -4133,7 +4097,7 @@ Cancellation token - + Subscribe to estimated funding rate kline updates @@ -4144,7 +4108,7 @@ Cancellation token - + Subscribe to incremental order book updates @@ -4156,7 +4120,7 @@ Cancellation token - + Subscribe to index kline updates @@ -4167,18 +4131,18 @@ Cancellation token - + Subscribe to kline updates - - + + Contract code Period Data handler Cancellation token - + Subscribe to mark price updates @@ -4189,7 +4153,7 @@ Cancellation token - + Subscribe to order book updates @@ -4200,7 +4164,7 @@ Cancellation token - + Subscribe to premium index kline updates @@ -4211,7 +4175,7 @@ Cancellation token - + Subscribe to symbol ticker updates @@ -4221,7 +4185,7 @@ Cancellation token - + Subscribe to symbol trade updates @@ -4294,6 +4258,11 @@ The data + + + The action + + The name of the data channel @@ -9676,5 +9645,39 @@ + + + Extensions for the ICryptoRestClient and ICryptoSocketClient interfaces + + + + + Get the Huobi REST Api client + + + + + + + Get the Huobi Websocket Api client + + + + + + + Extensions for DI + + + + + Add the IHuobiClient and IHuobiSocketClient to the sevice collection so they can be injected + + The service collection + Set default options for the rest client + Set default options for the socket client + The lifetime of the IHuobiSocketClient for the service collection. Defaults to Singleton. + + diff --git a/Huobi.Net/HuobiAuthenticationProvider.cs b/Huobi.Net/HuobiAuthenticationProvider.cs index c7ad8d99..fbfb7c16 100644 --- a/Huobi.Net/HuobiAuthenticationProvider.cs +++ b/Huobi.Net/HuobiAuthenticationProvider.cs @@ -1,15 +1,14 @@ using CryptoExchange.Net; using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Converters; using CryptoExchange.Net.Objects; using Huobi.Net.Objects.Internal; +using Huobi.Net.Objects.Sockets; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; -using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -63,7 +62,7 @@ public override void AuthenticateRequest(RestApiClient apiClient, uriParameters.Add("Signature", SignHMACSHA256(signData, SignOutputType.Base64)); } - public HuobiAuthenticationRequest GetWebsocketAuthentication(Uri uri) + public HuobiAuthParams GetWebsocketAuthentication(Uri uri) { var parameters = new Dictionary(); parameters.Add("accessKey", _credentials.Key!.GetString()); @@ -77,7 +76,7 @@ public HuobiAuthenticationRequest GetWebsocketAuthentication(Uri uri) var signData = $"GET\n{uri.Host}\n{uri.AbsolutePath}\n{paramString}"; var signature = SignHMACSHA256(signData, SignOutputType.Base64); - return new HuobiAuthenticationRequest(_credentials.Key!.GetString(), (string)parameters["timestamp"], signature); + return new HuobiAuthParams { AccessKey = _credentials.Key!.GetString(), Timestamp = (string)parameters["timestamp"], Signature = signature }; } public HuobiAuthenticationRequest2 GetWebsocketAuthentication2(Uri uri) diff --git a/Huobi.Net/Interfaces/Clients/IHuobiClient.cs b/Huobi.Net/Interfaces/Clients/IHuobiClient.cs index 3425a859..f61105c0 100644 --- a/Huobi.Net/Interfaces/Clients/IHuobiClient.cs +++ b/Huobi.Net/Interfaces/Clients/IHuobiClient.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; -using Huobi.Net.Clients.FuturesApi; using Huobi.Net.Interfaces.Clients.SpotApi; using Huobi.Net.Interfaces.Clients.UsdtMarginSwapApi; diff --git a/Huobi.Net/Interfaces/Clients/SpotApi/IHuobiSocketClientSpotApi.cs b/Huobi.Net/Interfaces/Clients/SpotApi/IHuobiSocketClientSpotApi.cs index 06ecb55f..69947298 100644 --- a/Huobi.Net/Interfaces/Clients/SpotApi/IHuobiSocketClientSpotApi.cs +++ b/Huobi.Net/Interfaces/Clients/SpotApi/IHuobiSocketClientSpotApi.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Objects.Sockets; using Huobi.Net.Enums; using Huobi.Net.Objects.Models; using Huobi.Net.Objects.Models.Socket; @@ -139,7 +139,7 @@ public interface IHuobiSocketClientSpotApi : ISocketApiClient, IDisposable /// The handler for updates /// Cancellation token for closing this subscription /// A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - Task> SubscribeToTickerUpdatesAsync(Action> onData, CancellationToken ct = default); + Task> SubscribeToTickerUpdatesAsync(Action>> onData, CancellationToken ct = default); /// /// Subscribe to changes of a symbol's best ask/bid @@ -149,8 +149,7 @@ public interface IHuobiSocketClientSpotApi : ISocketApiClient, IDisposable /// Data handler /// Cancellation token for closing this subscription /// A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected - Task> SubscribeToBestOfferUpdatesAsync(string symbol, - Action> onData, CancellationToken ct = default); + Task> SubscribeToBestOfferUpdatesAsync(string symbol, Action> onData, CancellationToken ct = default); /// /// Subscribe to updates of orders @@ -194,6 +193,5 @@ Task> SubscribeToOrderUpdatesAsync( /// A stream subscription. This stream subscription can be used to be notified when the socket is disconnected/reconnected Task> SubscribeToOrderDetailsUpdatesAsync(string? symbol = null, Action>? onOrderMatch = null, Action>? onOrderCancel = null, CancellationToken ct = default); - } } \ No newline at end of file diff --git a/Huobi.Net/Interfaces/Clients/UsdtMarginSwapApi/IHuobiSocketClientUsdtMarginSwapApi.cs b/Huobi.Net/Interfaces/Clients/UsdtMarginSwapApi/IHuobiSocketClientUsdtMarginSwapApi.cs index b8279ffa..37263db3 100644 --- a/Huobi.Net/Interfaces/Clients/UsdtMarginSwapApi/IHuobiSocketClientUsdtMarginSwapApi.cs +++ b/Huobi.Net/Interfaces/Clients/UsdtMarginSwapApi/IHuobiSocketClientUsdtMarginSwapApi.cs @@ -1,6 +1,6 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Objects.Sockets; using Huobi.Net.Enums; using Huobi.Net.Objects.Models; using Huobi.Net.Objects.Models.Socket; @@ -68,8 +68,8 @@ public interface IHuobiSocketClientUsdtMarginSwapApi : ISocketApiClient Task> SubscribeToIndexKlineUpdatesAsync(string contractCode, KlineInterval period, Action> onData, CancellationToken ct = default); /// /// Subscribe to kline updates - /// - /// + /// + /// /// Contract code /// Period /// Data handler diff --git a/Huobi.Net/Objects/Internal/HuobiSocketUpdate.cs b/Huobi.Net/Objects/Internal/HuobiSocketUpdate.cs index a5460e9a..b27f38b6 100644 --- a/Huobi.Net/Objects/Internal/HuobiSocketUpdate.cs +++ b/Huobi.Net/Objects/Internal/HuobiSocketUpdate.cs @@ -6,6 +6,11 @@ namespace Huobi.Net.Objects.Internal { internal class HuobiDataEvent { + /// + /// The action + /// + [JsonProperty("action")] + public string Action { get; set; } = string.Empty; /// /// The name of the data channel /// diff --git a/Huobi.Net/Objects/Models/HuobiConditionalOrderCancelResult.cs b/Huobi.Net/Objects/Models/HuobiConditionalOrderCancelResult.cs index 0c194237..124c6dce 100644 --- a/Huobi.Net/Objects/Models/HuobiConditionalOrderCancelResult.cs +++ b/Huobi.Net/Objects/Models/HuobiConditionalOrderCancelResult.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models { diff --git a/Huobi.Net/Objects/Models/HuobiOpenOrder.cs b/Huobi.Net/Objects/Models/HuobiOpenOrder.cs index e7561bc3..54dccf58 100644 --- a/Huobi.Net/Objects/Models/HuobiOpenOrder.cs +++ b/Huobi.Net/Objects/Models/HuobiOpenOrder.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using CryptoExchange.Net.Converters; using Huobi.Net.Converters; using Huobi.Net.Enums; diff --git a/Huobi.Net/Objects/Models/HuobiOrder.cs b/Huobi.Net/Objects/Models/HuobiOrder.cs index 79d0e78d..ca8ac755 100644 --- a/Huobi.Net/Objects/Models/HuobiOrder.cs +++ b/Huobi.Net/Objects/Models/HuobiOrder.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using CryptoExchange.Net.Converters; using Huobi.Net.Converters; using Huobi.Net.Enums; diff --git a/Huobi.Net/Objects/Models/HuobiOrderBook.cs b/Huobi.Net/Objects/Models/HuobiOrderBook.cs index f6cc1068..2af2ce8d 100644 --- a/Huobi.Net/Objects/Models/HuobiOrderBook.cs +++ b/Huobi.Net/Objects/Models/HuobiOrderBook.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using CryptoExchange.Net.Converters; -using CryptoExchange.Net.Interfaces; using Newtonsoft.Json; namespace Huobi.Net.Objects.Models diff --git a/Huobi.Net/Objects/Models/HuobiRepayment.cs b/Huobi.Net/Objects/Models/HuobiRepayment.cs index e4a09763..08ad75ab 100644 --- a/Huobi.Net/Objects/Models/HuobiRepayment.cs +++ b/Huobi.Net/Objects/Models/HuobiRepayment.cs @@ -1,8 +1,6 @@ using CryptoExchange.Net.Converters; using Newtonsoft.Json; using System; -using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models { diff --git a/Huobi.Net/Objects/Models/HuobiSymbolTrade.cs b/Huobi.Net/Objects/Models/HuobiSymbolTrade.cs index 7879c0ed..8ecb086e 100644 --- a/Huobi.Net/Objects/Models/HuobiSymbolTrade.cs +++ b/Huobi.Net/Objects/Models/HuobiSymbolTrade.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using CryptoExchange.Net.Converters; using Huobi.Net.Converters; using Huobi.Net.Enums; diff --git a/Huobi.Net/Objects/Models/Socket/HuobiOrderBookUpdate.cs b/Huobi.Net/Objects/Models/Socket/HuobiOrderBookUpdate.cs index 34559fe3..06c602de 100644 --- a/Huobi.Net/Objects/Models/Socket/HuobiOrderBookUpdate.cs +++ b/Huobi.Net/Objects/Models/Socket/HuobiOrderBookUpdate.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.Socket { diff --git a/Huobi.Net/Objects/Models/Socket/HuobiUsdtMarginSwapTradeUpdate.cs b/Huobi.Net/Objects/Models/Socket/HuobiUsdtMarginSwapTradeUpdate.cs index 2378f09d..c3349199 100644 --- a/Huobi.Net/Objects/Models/Socket/HuobiUsdtMarginSwapTradeUpdate.cs +++ b/Huobi.Net/Objects/Models/Socket/HuobiUsdtMarginSwapTradeUpdate.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.Socket { diff --git a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiBatchResult.cs b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiBatchResult.cs index b866200d..88a893e7 100644 --- a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiBatchResult.cs +++ b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiBatchResult.cs @@ -1,7 +1,5 @@ using Newtonsoft.Json; -using System; using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.UsdtMarginSwap { diff --git a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiEstimatedSettlementPrice.cs b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiEstimatedSettlementPrice.cs index 7803998e..9dd8f444 100644 --- a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiEstimatedSettlementPrice.cs +++ b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiEstimatedSettlementPrice.cs @@ -1,9 +1,6 @@ using CryptoExchange.Net.Converters; using Huobi.Net.Enums; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.UsdtMarginSwap { diff --git a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiIsolatedMarginAssetsAndPositions.cs b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiIsolatedMarginAssetsAndPositions.cs index 9d3e03f0..0eaee87a 100644 --- a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiIsolatedMarginAssetsAndPositions.cs +++ b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiIsolatedMarginAssetsAndPositions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.UsdtMarginSwap { diff --git a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiPriceLimitation.cs b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiPriceLimitation.cs index a377469c..64f1cdcd 100644 --- a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiPriceLimitation.cs +++ b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiPriceLimitation.cs @@ -1,9 +1,6 @@ using CryptoExchange.Net.Converters; using Huobi.Net.Enums; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.UsdtMarginSwap { diff --git a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiTradingFee.cs b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiTradingFee.cs index d585ff86..4e8b3f26 100644 --- a/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiTradingFee.cs +++ b/Huobi.Net/Objects/Models/UsdtMarginSwap/HuobiTradingFee.cs @@ -1,9 +1,6 @@ using CryptoExchange.Net.Converters; using Huobi.Net.Enums; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace Huobi.Net.Objects.Models.UsdtMarginSwap { diff --git a/Huobi.Net/Objects/Internal/HuobiAuthenticationRequest.cs b/Huobi.Net/Objects/Sockets/HuobiAuthParams.cs similarity index 53% rename from Huobi.Net/Objects/Internal/HuobiAuthenticationRequest.cs rename to Huobi.Net/Objects/Sockets/HuobiAuthParams.cs index 56dc8efd..2016e78b 100644 --- a/Huobi.Net/Objects/Internal/HuobiAuthenticationRequest.cs +++ b/Huobi.Net/Objects/Sockets/HuobiAuthParams.cs @@ -1,22 +1,7 @@ using Newtonsoft.Json; -namespace Huobi.Net.Objects.Internal +namespace Huobi.Net.Objects.Sockets { - internal class HuobiAuthenticationRequest: HuobiAuthenticatedSubscribeRequest - { - [JsonProperty("params")] public HuobiAuthParams Parameters { get; set; } - - public HuobiAuthenticationRequest(string accessKey, string timestamp, string signature): base("auth", "req") - { - Parameters = new HuobiAuthParams() - { - AccessKey = accessKey, - Timestamp = timestamp, - Signature = signature - }; - } - } - internal class HuobiAuthParams { [JsonProperty("authType")] public string AuthType { get; set; } = "api"; diff --git a/Huobi.Net/Objects/Sockets/HuobiAuthPongMessage.cs b/Huobi.Net/Objects/Sockets/HuobiAuthPongMessage.cs new file mode 100644 index 00000000..b1fca720 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiAuthPongMessage.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiAuthPongMessage + { + [JsonProperty("action")] + public string Action { get; set; } = string.Empty; + [JsonProperty("data")] + public HuobiAuthPongMessageTimestamp Data { get; set; } = null!; + } + + internal class HuobiAuthPingMessage + { + [JsonProperty("action")] + public string Action { get; set; } = string.Empty; + [JsonProperty("data")] + public HuobiAuthPingMessageTimestamp Data { get; set; } = null!; + } + + internal class HuobiAuthPongMessageTimestamp + { + [JsonProperty("pong")] + public long Pong { get; set; } + } + + internal class HuobiAuthPingMessageTimestamp + { + [JsonProperty("ts")] + public long Ping { get; set; } + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiAuthRequest.cs b/Huobi.Net/Objects/Sockets/HuobiAuthRequest.cs new file mode 100644 index 00000000..d534757e --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiAuthRequest.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiAuthRequest + { + [JsonProperty("action")] + public string Action { get; set; } = string.Empty; + [JsonProperty("ch")] + public string Channel { get; set; } = string.Empty; + } + + internal class HuobiAuthRequest : HuobiAuthRequest + { + [JsonProperty("params")] + public T Params { get; set; } = default!; + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiPingMessage.cs b/Huobi.Net/Objects/Sockets/HuobiPingMessage.cs new file mode 100644 index 00000000..3a15bc3e --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiPingMessage.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiPingMessage + { + [JsonProperty("ping")] + public long Ping { get; set; } + } + + internal class HuobiSpotPingWrapper + { + [JsonProperty("data")] + public HuobiSpotPingMessage Data { get; set; } = null!; + } + + internal class HuobiSpotPingMessage + { + [JsonProperty("ts")] + public long Ping { get; set; } + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiPongMessage.cs b/Huobi.Net/Objects/Sockets/HuobiPongMessage.cs new file mode 100644 index 00000000..19b6565e --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiPongMessage.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiPongMessage + { + [JsonProperty("pong")] + public long Pong { get; set; } + } + + internal class HuobiSpotPongMessage + { + [JsonProperty("action")] + public string Action { get; set; } = "pong"; + [JsonProperty("data")] + public HuobiSpotPingMessage Pong { get; set; } = null!; + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiSocketAuthResponse.cs b/Huobi.Net/Objects/Sockets/HuobiSocketAuthResponse.cs new file mode 100644 index 00000000..ed5d8850 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiSocketAuthResponse.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiSocketAuthResponse + { + [JsonProperty("action")] + public string Action { get; set; } = string.Empty; + [JsonProperty("ch")] + public string Channel { get; set; } = string.Empty; + [JsonProperty("code")] + public int Code { get; set; } + [JsonProperty("message")] + public string? Message { get; set; } + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiSocketResponse.cs b/Huobi.Net/Objects/Sockets/HuobiSocketResponse.cs new file mode 100644 index 00000000..6761c517 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiSocketResponse.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiSocketResponse + { + public string Id { get; set; } = string.Empty; + public string? Status { get; set; } + [JsonProperty("err-code")] + public string? ErrorCode { get; set; } + [JsonProperty("err-msg")] + public string? ErrorMessage { get; set; } + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiSubscribeRequest.cs b/Huobi.Net/Objects/Sockets/HuobiSubscribeRequest.cs new file mode 100644 index 00000000..9c4798e6 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiSubscribeRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiSubscribeRequest + { + [JsonProperty("sub")] + public string Topic { get; set; } = string.Empty; + [JsonProperty("id")] + public string Id { get; set; } = string.Empty; + } +} diff --git a/Huobi.Net/Objects/Sockets/HuobiUnsubscribeRequest.cs b/Huobi.Net/Objects/Sockets/HuobiUnsubscribeRequest.cs new file mode 100644 index 00000000..e8e4ed3c --- /dev/null +++ b/Huobi.Net/Objects/Sockets/HuobiUnsubscribeRequest.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Huobi.Net.Objects.Sockets +{ + internal class HuobiUnsubscribeRequest + { + [JsonProperty("unsub")] + public string Topic { get; set; } = string.Empty; + [JsonProperty("id")] + public string Id { get; set; } = string.Empty; + } +} diff --git a/Huobi.Net/Objects/Sockets/Queries/HuobiAuthQuery.cs b/Huobi.Net/Objects/Sockets/Queries/HuobiAuthQuery.cs new file mode 100644 index 00000000..022c10fb --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Queries/HuobiAuthQuery.cs @@ -0,0 +1,29 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Queries +{ + internal class HuobiAuthQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } + public HuobiAuthQuery(string action, string topic, bool authenticated, int weight = 1) : base(new HuobiAuthRequest() { Action = action, Channel = topic }, authenticated, weight) + { + ListenerIdentifiers = new HashSet { action + topic }; + } + public HuobiAuthQuery(HuobiAuthRequest request) : base(request, true, 1) + { + ListenerIdentifiers = new HashSet { request.Action + request.Channel }; + } + + public override Task> HandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data.Code != 200) + return Task.FromResult(new CallResult(new ServerError(message.Data.Code, message.Data.Message!))); + + return base.HandleMessageAsync(connection, message); + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Queries/HuobiQuery.cs b/Huobi.Net/Objects/Sockets/Queries/HuobiQuery.cs new file mode 100644 index 00000000..6a4b3b96 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Queries/HuobiQuery.cs @@ -0,0 +1,28 @@ +using CryptoExchange.Net; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using Huobi.Net.Objects.Internal; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Queries +{ + internal class HuobiQuery : Query> + { + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiQuery(string topic, bool authenticated, int weight = 1) : base(new HuobiSocketRequest(ExchangeHelpers.NextId().ToString(), topic), authenticated, weight) + { + ListenerIdentifiers = new HashSet { ((HuobiSocketRequest)Request).Id }; + } + + public override Task>> HandleMessageAsync(SocketConnection connection, DataEvent> message) + { + if (message.Data.IsSuccessful) + return Task.FromResult(new CallResult>(message.Data, message.OriginalData, null)); + + return Task.FromResult(new CallResult>(new ServerError(message.Data.ErrorCode!, message.Data.ErrorMessage))); + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Queries/HuobiSubscribeQuery.cs b/Huobi.Net/Objects/Sockets/Queries/HuobiSubscribeQuery.cs new file mode 100644 index 00000000..cc89e3ce --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Queries/HuobiSubscribeQuery.cs @@ -0,0 +1,27 @@ +using CryptoExchange.Net; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Queries +{ + internal class HuobiSubscribeQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiSubscribeQuery(string topic, bool authenticated, int weight = 1) : base(new HuobiSubscribeRequest() { Id = ExchangeHelpers.NextId().ToString(), Topic = topic }, authenticated, weight) + { + ListenerIdentifiers = new HashSet { ((HuobiSubscribeRequest)Request).Id }; + } + + public override Task> HandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data.Status != "ok") + return Task.FromResult(new CallResult(new ServerError(message.Data.ErrorMessage!))); + + return Task.FromResult(new CallResult(message.Data, message.OriginalData, null)); + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Queries/HuobiUnsubscribeQuery.cs b/Huobi.Net/Objects/Sockets/Queries/HuobiUnsubscribeQuery.cs new file mode 100644 index 00000000..ad4e0e15 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Queries/HuobiUnsubscribeQuery.cs @@ -0,0 +1,17 @@ +using CryptoExchange.Net; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; + +namespace Huobi.Net.Objects.Sockets.Queries +{ + internal class HuobiUnsubscribeQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiUnsubscribeQuery(string topic, bool authenticated, int weight = 1) : base(new HuobiUnsubscribeRequest() { Id = ExchangeHelpers.NextId().ToString(), Topic = topic }, authenticated, weight) + { + ListenerIdentifiers = new HashSet { ((HuobiUnsubscribeRequest)Request).Id }; + } + + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiAccountSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiAccountSubscription.cs new file mode 100644 index 00000000..b766507e --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiAccountSubscription.cs @@ -0,0 +1,46 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using Huobi.Net.Objects.Internal; +using Huobi.Net.Objects.Models.Socket; +using Huobi.Net.Objects.Sockets.Queries; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiAccountSubscription : Subscription + { + private string _topic; + private Action> _handler; + + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiAccountSubscription(ILogger logger, string topic, Action> handler, bool authenticated) : base(logger, authenticated) + { + _handler = handler; + _topic = topic; + ListenerIdentifiers = new HashSet() { topic }; + } + + public override Query? GetSubQuery(SocketConnection connection) + { + return new HuobiAuthQuery("sub", _topic, Authenticated); + } + public override Query? GetUnsubQuery() + { + return new HuobiAuthQuery("unsub", _topic, Authenticated); + } + public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + { + var update = (HuobiDataEvent)message.Data; + _handler.Invoke(message.As(update.Data, update.Channel)); + return Task.FromResult(new CallResult(null)); + } + + public override Type? GetMessageType(IMessageAccessor message) => typeof(HuobiDataEvent); + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiAuthPingSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiAuthPingSubscription.cs new file mode 100644 index 00000000..7378cb4b --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiAuthPingSubscription.cs @@ -0,0 +1,25 @@ +using CryptoExchange.Net; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiAuthPingSubscription : SystemSubscription + { + public override HashSet ListenerIdentifiers { get; set; } = new HashSet() { "pingv2" }; + + public HuobiAuthPingSubscription(ILogger logger) : base(logger, false) + { + } + + public override Task HandleMessageAsync(SocketConnection connection, DataEvent message) + { + connection.Send(ExchangeHelpers.NextId(), new HuobiAuthPongMessage() { Action = "pong", Data = new HuobiAuthPongMessageTimestamp { Pong = message.Data.Data.Ping } }, 1); + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderDetailsSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderDetailsSubscription.cs new file mode 100644 index 00000000..85d8ef47 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderDetailsSubscription.cs @@ -0,0 +1,66 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.MessageParsing; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using Huobi.Net.Objects.Internal; +using Huobi.Net.Objects.Models.Socket; +using Huobi.Net.Objects.Sockets.Queries; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiOrderDetailsSubscription : Subscription + { + private string _topic; + private Action>? _onOrderMatch; + private Action>? _onOrderCancel; + + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiOrderDetailsSubscription( + ILogger logger, + string? symbol, + Action>? onOrderMatch, + Action>? onOrderCancel) : base(logger, true) + { + _topic = $"trade.clearing#{symbol ?? "*"}#1"; + _onOrderMatch = onOrderMatch; + _onOrderCancel = onOrderCancel; + ListenerIdentifiers = new HashSet() { _topic }; + } + + public override Query? GetSubQuery(SocketConnection connection) + { + return new HuobiAuthQuery("sub", _topic, Authenticated); + } + public override Query? GetUnsubQuery() + { + return new HuobiAuthQuery("unsub", _topic, Authenticated); + } + public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + { + var data = message.Data; + if (data is HuobiDataEvent tradeEvent) + _onOrderMatch?.Invoke(message.As(tradeEvent.Data, tradeEvent.Channel)); + if (data is HuobiDataEvent cancelEvent) + _onOrderCancel?.Invoke(message.As(cancelEvent.Data, cancelEvent.Channel)); + return Task.FromResult(new CallResult(null)); + } + + public override Type? GetMessageType(IMessageAccessor message) + { + var typePath = MessagePath.Get().Property("data").Property("eventType"); + var eventType = message.GetValue(typePath); + if (eventType == "trade") + return typeof(HuobiDataEvent); + if (eventType == "cancellation") + return typeof(HuobiDataEvent); + + return null; + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderSubscription.cs new file mode 100644 index 00000000..9595a4af --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiOrderSubscription.cs @@ -0,0 +1,87 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.MessageParsing; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using Huobi.Net.Objects.Internal; +using Huobi.Net.Objects.Models.Socket; +using Huobi.Net.Objects.Sockets.Queries; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiOrderSubscription : Subscription + { + private string _topic; + private Action>? _onOrderSubmitted; + private Action>? _onOrderMatched; + private Action>? _onOrderCancelation; + private Action>? _onConditionalOrderTriggerFailure; + private Action>? _onConditionalOrderCanceled; + + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiOrderSubscription( + ILogger logger, + string? symbol, + Action>? onOrderSubmitted, + Action>? onOrderMatched, + Action>? onOrderCancelation, + Action>? onConditionalOrderTriggerFailure, + Action>? onConditionalOrderCanceled) : base(logger, true) + { + _topic = $"orders#{symbol ?? "*"}"; + _onOrderSubmitted = onOrderSubmitted; + _onOrderMatched = onOrderMatched; + _onOrderCancelation = onOrderCancelation; + _onConditionalOrderTriggerFailure = onConditionalOrderTriggerFailure; + _onConditionalOrderCanceled = onConditionalOrderCanceled; + ListenerIdentifiers = new HashSet() { _topic }; + } + + public override Query? GetSubQuery(SocketConnection connection) + { + return new HuobiAuthQuery("sub", _topic, Authenticated); + } + public override Query? GetUnsubQuery() + { + return new HuobiAuthQuery("unsub", _topic, Authenticated); + } + public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + { + var data = message.Data; + if (data is HuobiDataEvent triggerFailEvent) + _onConditionalOrderTriggerFailure?.Invoke(message.As(triggerFailEvent.Data, triggerFailEvent.Channel)); + if (data is HuobiDataEvent orderEvent) + _onConditionalOrderCanceled?.Invoke(message.As(orderEvent.Data, orderEvent.Channel)); + if (data is HuobiDataEvent submitOrderEvent) + _onOrderSubmitted?.Invoke(message.As(submitOrderEvent.Data, submitOrderEvent.Channel)); + if (data is HuobiDataEvent matchOrderEvent) + _onOrderMatched?.Invoke(message.As(matchOrderEvent.Data, matchOrderEvent.Channel)); + if (data is HuobiDataEvent cancelOrderEvent) + _onOrderCancelation?.Invoke(message.As(cancelOrderEvent.Data, cancelOrderEvent.Channel)); + return Task.FromResult(new CallResult(null)); + } + + public override Type? GetMessageType(IMessageAccessor message) + { + var typePath = MessagePath.Get().Property("data").Property("eventType"); + var eventType = message.GetValue(typePath); + if (eventType == "trigger") + return typeof(HuobiDataEvent); + if (eventType == "deletion") + return typeof(HuobiDataEvent); + if (eventType == "creation") + return typeof(HuobiDataEvent); + if (eventType == "trade") + return typeof(HuobiDataEvent); + if (eventType == "cancellation") + return typeof(HuobiDataEvent); + + return null; + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiPingSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiPingSubscription.cs new file mode 100644 index 00000000..2ad59fe3 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiPingSubscription.cs @@ -0,0 +1,25 @@ +using CryptoExchange.Net; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiPingSubscription : SystemSubscription + { + public override HashSet ListenerIdentifiers { get; set; } = new HashSet() { "pingV3" }; + + public HuobiPingSubscription(ILogger logger) : base(logger, false) + { + } + + public override Task HandleMessageAsync(SocketConnection connection, DataEvent message) + { + connection.Send(ExchangeHelpers.NextId(), new HuobiPongMessage() { Pong = message.Data.Ping }, 1); + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiSpotPingSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiSpotPingSubscription.cs new file mode 100644 index 00000000..ba8efe80 --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiSpotPingSubscription.cs @@ -0,0 +1,25 @@ +using CryptoExchange.Net; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiSpotPingSubscription : SystemSubscription + { + public override HashSet ListenerIdentifiers { get; set; } = new HashSet() { "pingV2" }; + + public HuobiSpotPingSubscription(ILogger logger) : base(logger, false) + { + } + + public override Task HandleMessageAsync(SocketConnection connection, DataEvent message) + { + connection.Send(ExchangeHelpers.NextId(), new HuobiSpotPongMessage() { Pong = new HuobiSpotPingMessage { Ping = message.Data.Data.Ping } }, 1); + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Huobi.Net/Objects/Sockets/Subscriptions/HuobiSubscription.cs b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiSubscription.cs new file mode 100644 index 00000000..1a5b3f7e --- /dev/null +++ b/Huobi.Net/Objects/Sockets/Subscriptions/HuobiSubscription.cs @@ -0,0 +1,46 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using Huobi.Net.Objects.Internal; +using Huobi.Net.Objects.Sockets.Queries; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Huobi.Net.Objects.Sockets.Subscriptions +{ + internal class HuobiSubscription : Subscription + { + private string _topic; + private Action> _handler; + + public override HashSet ListenerIdentifiers { get; set; } + + public HuobiSubscription(ILogger logger, string topic, Action> handler, bool authenticated) : base(logger, authenticated) + { + _handler = handler; + _topic = topic; + ListenerIdentifiers = new HashSet() { topic }; + } + + public override Query? GetSubQuery(SocketConnection connection) + { + return new HuobiSubscribeQuery(_topic, Authenticated); + } + public override Query? GetUnsubQuery() + { + return new HuobiUnsubscribeQuery(_topic, Authenticated); + } + + public override Type? GetMessageType(IMessageAccessor message) => typeof(HuobiDataEvent); + + public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + { + var huobiEvent = (HuobiDataEvent)message.Data; + _handler.Invoke(message.As(huobiEvent.Data, huobiEvent.Channel)); + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Huobi.Net/SymbolOrderBooks/HuobiOrderBookFactory.cs b/Huobi.Net/SymbolOrderBooks/HuobiOrderBookFactory.cs index 31f63fef..8757876e 100644 --- a/Huobi.Net/SymbolOrderBooks/HuobiOrderBookFactory.cs +++ b/Huobi.Net/SymbolOrderBooks/HuobiOrderBookFactory.cs @@ -2,7 +2,6 @@ using Huobi.Net.Interfaces; using Huobi.Net.Interfaces.Clients; using Huobi.Net.Objects.Options; -using Huobi.Net.SymbolOrderBooks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; diff --git a/Huobi.Net/SymbolOrderBooks/HuobiSpotSymbolOrderBook.cs b/Huobi.Net/SymbolOrderBooks/HuobiSpotSymbolOrderBook.cs index d3dbc76e..8fc5f193 100644 --- a/Huobi.Net/SymbolOrderBooks/HuobiSpotSymbolOrderBook.cs +++ b/Huobi.Net/SymbolOrderBooks/HuobiSpotSymbolOrderBook.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using CryptoExchange.Net.Objects; using CryptoExchange.Net.OrderBook; -using CryptoExchange.Net.Sockets; using System; using Huobi.Net.Objects.Models; using Huobi.Net.Interfaces.Clients; @@ -9,6 +8,7 @@ using System.Threading; using Microsoft.Extensions.Logging; using Huobi.Net.Objects.Options; +using CryptoExchange.Net.Objects.Sockets; namespace Huobi.Net.SymbolOrderBooks { @@ -72,13 +72,13 @@ public HuobiSpotSymbolOrderBook(string symbol, /// protected override async Task> DoStartAsync(CancellationToken ct) { - if(_mergeStep != null) + if (_mergeStep != null) { var subResult = await _socketClient.SpotApi.SubscribeToPartialOrderBookUpdates1SecondAsync(Symbol, _mergeStep.Value, HandleUpdate).ConfigureAwait(false); if (!subResult) return subResult; - if(ct.IsCancellationRequested) + if (ct.IsCancellationRequested) { await subResult.Data.CloseAsync().ConfigureAwait(false); return subResult.AsError(new CancellationRequestedError()); @@ -107,7 +107,7 @@ protected override async Task> DoStartAsync(Cance // Wait a little so that the sequence number of the order book snapshot is higher than the first socket update sequence number await Task.Delay(500).ConfigureAwait(false); var book = await _socketClient.SpotApi.GetOrderBookAsync(Symbol, _levels.Value).ConfigureAwait(false); - if (!book) + if (!book) { _logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} failed to retrieve initial order book"); await _socketClient.UnsubscribeAsync(subResult.Data).ConfigureAwait(false); @@ -116,7 +116,7 @@ protected override async Task> DoStartAsync(Cance SetInitialOrderBook(book.Data.SequenceNumber, book.Data.Bids, book.Data.Asks); return subResult; - } + } } private void HandleIncremental(DataEvent book) @@ -145,7 +145,7 @@ protected override async Task> DoResyncAsync(CancellationToken await Task.Delay(5000).ConfigureAwait(false); var book = await _socketClient.SpotApi.GetOrderBookAsync(Symbol, _levels!.Value).ConfigureAwait(false); if (!book) - return new CallResult(book.Error!); + return new CallResult(book.Error!); SetInitialOrderBook(book.Data.SequenceNumber, book.Data.Bids!, book.Data.Asks!); return new CallResult(true); diff --git a/README.md b/README.md index 865590e7..c37189e4 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,113 @@ -# Huobi.Net -[![.NET](https://github.com/JKorf/Huobi.Net/actions/workflows/dotnet.yml/badge.svg)](https://github.com/JKorf/Huobi.Net/actions/workflows/dotnet.yml) [![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg)](https://www.nuget.org/packages/Huobi.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/Huobi.Net.svg)](https://www.nuget.org/packages/Huobi.Net) +# ![.Huobi.Net](https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true) Huobi.Net + +[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/Huobi.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/Huobi.Net/actions/workflows/dotnet.yml) ![License](https://img.shields.io/github/license/JKorf/Huobi.Net?style=for-the-badge) Huobi.Net is a wrapper around the Huobi API as described on [Huobi](https://github.com/huobiapi), including all features the API provides using clear and readable objects, both for the REST as the websocket API's. -**If you think something is broken, something is missing or have any questions, please open an [Issue](https://github.com/JKorf/Huobi.Net/issues)** +## Supported Frameworks +The library is targeting both `.NET Standard 2.0` and `.NET Standard 2.1` for optimal compatibility + +|.NET implementation|Version Support| +|--|--| +|.NET Core|`2.0` and higher| +|.NET Framework|`4.6.1` and higher| +|Mono|`5.4` and higher| +|Xamarin.iOS|`10.14` and higher| +|Xamarin.Android|`8.0` and higher| +|UWP|`10.0.16299` and higher| +|Unity|`2018.1` and higher| + +## Get the library +[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg?style=for-the-badge)](https://www.nuget.org/packages/Huobi.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/Huobi.Net.svg?style=for-the-badge)](https://www.nuget.org/packages/Huobi.Net) + + dotnet add package Huobi.Net + +## How to use +* REST Endpoints + ```csharp + // Get the ETH/USDT ticker via rest request + var restClient = new HuobiRestClient(); + var tickerResult = await restClient.SpotApi.ExchangeData.GetTickerAsync("ETHUSDT"); + var lastPrice = tickerResult.Data.ClosePrice; + ``` +* Websocket streams + ```csharp + // Subscribe to ETH/USDT ticker updates via the websocket API + var socketClient = new HuobiSocketClient(); + var tickerSubscriptionResult = socketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethusdt", (update) => + { + var lastPrice = update.Data.ClosePrice; + }); + ``` + +For information on the clients, dependency injection, response processing and more see the [documentation](https://jkorf.github.io/CryptoExchange.Net), or have a look at the examples [here](https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples). + +## CryptoExchange.Net +Huobi.Net is based on the [CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net) base library. Other exchange API implementations based on the CryptoExchange.Net base library are available and follow the same logic. + +CryptoExchange.Net also allows for [easy access to different exchange API's](https://jkorf.github.io/CryptoExchange.Net#idocs_common). + +|Exchange|Repository|Nuget| +|--|--|--| +|Binance|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square)](https://www.nuget.org/packages/Binance.Net)| +|Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)| +|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)| +|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)| +|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)| +|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)| +|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[![Nuget version](https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square)](https://www.nuget.org/packages/KrakenExchange.Net)| +|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)| +|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)| +|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)| -[Documentation](https://jkorf.github.io/Huobi.Net/) +## Discord +[![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](https://discord.gg/MSpeEtSY8t) +A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries. -## Installation -`dotnet add package Huobi.Net` +## Supported functionality + +### Spot Api +|API|Supported|Location| +|--|--:|--| +|Reference Data|✓|`restClient.SpotApi.ExchangeData`| +|Market Data|✓|`restClient.SpotApi.ExchangeData`| +|Account|✓|`restClient.SpotApi.Account`| +|Wallet|✓|`restClient.SpotApi.Account`| +|Sub user management|Partial|`restClient.SpotApi.Account`| +|Trading|✓|`restClient.SpotApi.Trading`| +|Conditional Order|✓|`restClient.SpotApi.Trading`| +|Margin Loan|✓|`restClient.SpotApi.Account`| +|Margin Loan|✓|`restClient.SpotApi.Account`| +|Websocket Market Data|✓|`socketClient.SpotApi`| +|Websocket Account and Order|✓|`socketClient.SpotApi`| + +### Coin-M Futures Api +|API|Supported|Location| +|--|--:|--| +|*|X|| + +### Coin-M Swap Api +|API|Supported|Location| +|--|--:|--| +|*|X|| + +### USDT-M Api +|API|Supported|Location| +|--|--:|--| +|Reference Data|✓|`restClient.UsdtMarginSwapApi.ExchangeData`| +|Swap Market Data Interface|✓|`restClient.UsdtMarginSwapApi.ExchangeData`| +|Swap Account Interface|✓|`restClient.UsdtMarginSwapApi.Account`| +|Swap Trade Interface|✓|`restClient.UsdtMarginSwapApi.Trading`| +|Swap Strategy Order Interface|X|| +|Swap Transferring Interface|X|`restClient.SpotApi.Account`| +|Websocket Market Interface|✓|`socketClient.UsdtMarginSwapApi`| +|Websocket Index and Basis Interface|✓|`socketClient.UsdtMarginSwapApi`| +|Orders And Account WebSocket|X|| +|WebSocket System updates|X|| ## Support the project I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated. -### Referral link -Sign up using the following referral link to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to Huobi. This doesn't cost you a thing! -[Link](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93) - ### Donate Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me. @@ -26,10 +117,16 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d ### Sponsor Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). -## Discord -A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries. - ## Release notes +* Version 5.1.0-beta2 - 18 Feb 2024 + * Fixed ping responses websockets + * Fixed lower case symbol usdt margin subscription handling + +* Version 5.1.0-beta1 - 06 Feb 2024 + * Updated CryptoExchange.Net and implemented reworked websocket message handling. For release notes for the CryptoExchange.Net base library see: https://github.com/JKorf/CryptoExchange.Net/tree/beta?tab=readme-ov-file#release-notes + * Fixed issue in DI registration causing http client to not be correctly injected + * Removed excessive constructor overload for HuobiRestClient + * Version 5.0.5 - 03 Dec 2023 * Updated CryptoExchange.Net