From 68c45ef69711734cb9663ae57b7c40ad4f2e9908 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Sat, 24 Feb 2024 22:40:50 +0100 Subject: [PATCH] Websocket refactoring (#155) Websocket refactoring --- Bitfinex.Net.UnitTests/BitfinexClientTests.cs | 2 + .../BitfinexSocketClientTests.cs | 444 +++++---- .../TestImplementations/TestHelpers.cs | 1 + .../TestImplementations/TestSocket.cs | 36 +- Bitfinex.Net/Bitfinex.Net.csproj | 14 +- Bitfinex.Net/Bitfinex.Net.xml | 260 +++--- .../BitfinexAuthenticationProvider.cs | 2 - Bitfinex.Net/Clients/BitfinexRestClient.cs | 10 +- Bitfinex.Net/Clients/BitfinexSocketClient.cs | 13 +- .../BitfinexRestClientGeneralApiFunding.cs | 1 + .../BitfinexRestClientSpotApiAccount.cs | 1 + .../BitfinexRestClientSpotApiExchangeData.cs | 1 + .../BitfinexRestClientSpotApiTrading.cs | 2 +- .../SpotApi/BitfinexSocketClientSpotApi.cs | 871 +++--------------- .../Converters/OrderBookEntryConverter.cs | 7 +- Bitfinex.Net/Enums/BitfinexEventType.cs | 74 +- .../BitfinexExtensionMethods.cs | 51 + .../CryptoClientExtensions.cs | 25 + .../ServiceCollectionExtensions.cs} | 56 +- .../SpotApi/IBitfinexSocketClientSpotApi.cs | 71 +- .../BitfinexAuthenticationResponse.cs | 43 - .../Objects/Internal/BitfinexChecksum.cs | 16 + .../Objects/Internal/BitfinexEvents.cs | 5 - .../Objects/Internal/BitfinexResponse.cs | 24 - .../Objects/Internal/BitfinexSocketConfig.cs | 3 +- .../Objects/Models/BitfinexBalance.cs | 24 + .../Models/BitfinexFundingAutoRenew.cs | 1 - .../Models/BitfinexFundingOrderBook.cs | 1 - .../Objects/Models/BitfinexKeyValue.cs | 3 - .../Objects/Models/BitfinexOrderBook.cs | 1 - .../Objects/Models/BitfinexOrderBookEntry.cs | 4 +- .../Models/BitfinexRawFundingOrderBook.cs | 1 - .../Objects/Models/BitfinexRawOrderBook.cs | 1 - .../Objects/Models/BitfinexSummary.cs | 3 +- .../Objects/Models/BitfinexTradeSimple.cs | 7 - .../Models/Socket/BitfinexNotification.cs | 24 + .../Models/Socket/BitfinexSocketEvent.cs | 6 +- .../Models/Socket/BitfinexSocketInfo.cs | 29 + .../Objects/Sockets/BitfinexBookRequest.cs | 16 + .../Objects/Sockets/BitfinexConfQuery.cs | 16 + .../Objects/Sockets/BitfinexRequest.cs | 14 + .../Objects/Sockets/BitfinexResponse.cs | 24 + .../Objects/Sockets/BitfinexUpdate.cs | 28 + .../Sockets/Queries/BitfinexAuthQuery.cs | 26 + .../Objects/Sockets/Queries/BitfinexQuery.cs | 39 + .../Sockets/Queries/BitfinexSubQuery.cs | 42 + .../Sockets/Queries/BitfinexUnsubQuery.cs | 19 + .../Subscriptions/BitfinexInfoSubscription.cs | 52 ++ .../Subscriptions/BitfinexSubscription.cs | 113 +++ .../Subscriptions/BitfinexUserSubscription.cs | 183 ++++ .../BitfinexSymbolOrderBook.cs | 14 +- README.md | 96 +- 52 files changed, 1460 insertions(+), 1360 deletions(-) create mode 100644 Bitfinex.Net/ExtensionMethods/BitfinexExtensionMethods.cs create mode 100644 Bitfinex.Net/ExtensionMethods/CryptoClientExtensions.cs rename Bitfinex.Net/{BitfinexHelpers.cs => ExtensionMethods/ServiceCollectionExtensions.cs} (56%) delete mode 100644 Bitfinex.Net/Objects/Internal/BitfinexAuthenticationResponse.cs create mode 100644 Bitfinex.Net/Objects/Internal/BitfinexChecksum.cs delete mode 100644 Bitfinex.Net/Objects/Internal/BitfinexResponse.cs create mode 100644 Bitfinex.Net/Objects/Models/BitfinexBalance.cs create mode 100644 Bitfinex.Net/Objects/Models/Socket/BitfinexNotification.cs create mode 100644 Bitfinex.Net/Objects/Models/Socket/BitfinexSocketInfo.cs create mode 100644 Bitfinex.Net/Objects/Sockets/BitfinexBookRequest.cs create mode 100644 Bitfinex.Net/Objects/Sockets/BitfinexConfQuery.cs create mode 100644 Bitfinex.Net/Objects/Sockets/BitfinexRequest.cs create mode 100644 Bitfinex.Net/Objects/Sockets/BitfinexResponse.cs create mode 100644 Bitfinex.Net/Objects/Sockets/BitfinexUpdate.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Queries/BitfinexAuthQuery.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Queries/BitfinexQuery.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Queries/BitfinexSubQuery.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Queries/BitfinexUnsubQuery.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexInfoSubscription.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexSubscription.cs create mode 100644 Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexUserSubscription.cs diff --git a/Bitfinex.Net.UnitTests/BitfinexClientTests.cs b/Bitfinex.Net.UnitTests/BitfinexClientTests.cs index 6d8c794..2e52db5 100644 --- a/Bitfinex.Net.UnitTests/BitfinexClientTests.cs +++ b/Bitfinex.Net.UnitTests/BitfinexClientTests.cs @@ -13,6 +13,8 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; using Bitfinex.Net.Clients; +using Bitfinex.Net.ExtensionMethods; +using CryptoExchange.Net.Objects.Sockets; namespace Bitfinex.Net.UnitTests { diff --git a/Bitfinex.Net.UnitTests/BitfinexSocketClientTests.cs b/Bitfinex.Net.UnitTests/BitfinexSocketClientTests.cs index f08fc4c..8eca51f 100644 --- a/Bitfinex.Net.UnitTests/BitfinexSocketClientTests.cs +++ b/Bitfinex.Net.UnitTests/BitfinexSocketClientTests.cs @@ -17,6 +17,8 @@ using Bitfinex.Net.Objects.Internal; using Bitfinex.Net.Objects.Models; using Bitfinex.Net.Objects.Models.Socket; +using System.Diagnostics; +using Bitfinex.Net.Objects.Sockets; namespace Bitfinex.Net.UnitTests { @@ -54,7 +56,7 @@ public async Task SubscribingToBookUpdates_Should_SubscribeSuccessfully(Precisio }; // act - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); var taskResult = await subTask.ConfigureAwait(false); @@ -70,7 +72,7 @@ public async Task SubscribingToBookUpdates_Should_SubscribeSuccessfully(Precisio [TestCase(Precision.PrecisionLevel1, Frequency.TwoSeconds)] [TestCase(Precision.PrecisionLevel2, Frequency.TwoSeconds)] [TestCase(Precision.PrecisionLevel3, Frequency.TwoSeconds)] - public void SubscribingToBookUpdates_Should_TriggerWithBookUpdate(Precision prec, Frequency freq) + public async Task SubscribingToBookUpdates_Should_TriggerWithBookUpdate(Precision prec, Frequency freq) { // arrange var socket = new TestSocket(); @@ -91,12 +93,12 @@ public void SubscribingToBookUpdates_Should_TriggerWithBookUpdate(Precision prec Precision = JsonConvert.SerializeObject(prec, new PrecisionConverter(false)), Symbol = "tBTCUSD" }; - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); - BitfinexOrderBookEntry[] expected = new[] { new BitfinexOrderBookEntry() }; + BitfinexOrderBookEntry[] expected = new[] { new BitfinexOrderBookEntry() { RawPrice = "1", RawQuantity = "2", Count = 3, Price = 1, Quantity = 2 } }; // act - socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); + await socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); // assert Assert.IsTrue(TestHelpers.AreEqual(result[0], expected[0])); @@ -113,7 +115,7 @@ public void SubscribingToBookUpdates_Should_TriggerWithBookUpdate(Precision prec [TestCase(KlineInterval.OneDay)] [TestCase(KlineInterval.SevenDays)] [TestCase(KlineInterval.FourteenDays)] - public void SubscribingToCandleUpdates_Should_SubscribeSuccessfully(KlineInterval timeframe) + public async Task SubscribingToCandleUpdates_Should_SubscribeSuccessfully(KlineInterval timeframe) { // arrange var socket = new TestSocket(); @@ -127,12 +129,11 @@ public void SubscribingToCandleUpdates_Should_SubscribeSuccessfully(KlineInterva Channel = "candles", Event = "subscribed", ChannelId = 1, - Symbol = "BTCUSD", Key = "trade:" + JsonConvert.SerializeObject(timeframe, new KlineIntervalConverter(false)) + ":tBTCUSD" }; // act - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); @@ -151,7 +152,7 @@ public void SubscribingToCandleUpdates_Should_SubscribeSuccessfully(KlineInterva [TestCase(KlineInterval.OneDay)] [TestCase(KlineInterval.SevenDays)] [TestCase(KlineInterval.FourteenDays)] - public void SubscribingToCandleUpdates_Should_TriggerWithCandleUpdate(KlineInterval timeframe) + public async Task SubscribingToCandleUpdates_Should_TriggerWithCandleUpdate(KlineInterval timeframe) { // arrange var socket = new TestSocket(); @@ -166,22 +167,21 @@ public void SubscribingToCandleUpdates_Should_TriggerWithCandleUpdate(KlineInter Channel = "candles", Event = "subscribed", ChannelId = 1, - Symbol = "BTCUSD", Key = "trade:" + JsonConvert.SerializeObject(timeframe, new KlineIntervalConverter(false)) + ":tBTCUSD" }; - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); BitfinexKline[] expected = new[] { new BitfinexKline() }; // act - socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); + await socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); // assert Assert.IsTrue(TestHelpers.AreEqual(result[0], expected[0])); } [Test] - public void SubscribingToTickerUpdates_Should_SubscribeSuccessfully() + public async Task SubscribingToTickerUpdates_Should_SubscribeSuccessfully() { // arrange var socket = new TestSocket(); @@ -200,7 +200,7 @@ public void SubscribingToTickerUpdates_Should_SubscribeSuccessfully() }; // act - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); @@ -209,7 +209,7 @@ public void SubscribingToTickerUpdates_Should_SubscribeSuccessfully() } [Test] - public void SubscribingToTickerUpdates_Should_TriggerWithTickerUpdate() + public async Task SubscribingToTickerUpdates_Should_TriggerWithTickerUpdate() { // arrange var socket = new TestSocket(); @@ -227,19 +227,19 @@ public void SubscribingToTickerUpdates_Should_TriggerWithTickerUpdate() Symbol = "tBTCUSD", Pair = "BTCUSD" }; - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); BitfinexStreamTicker expected = new BitfinexStreamTicker(); // act - socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); + await socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); // assert Assert.IsTrue(TestHelpers.AreEqual(result, expected)); } [Test] - public void SubscribingToRawBookUpdates_Should_SubscribeSuccessfully() + public async Task SubscribingToRawBookUpdates_Should_SubscribeSuccessfully() { // arrange var socket = new TestSocket(); @@ -261,7 +261,7 @@ public void SubscribingToRawBookUpdates_Should_SubscribeSuccessfully() }; // act - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); @@ -270,7 +270,7 @@ public void SubscribingToRawBookUpdates_Should_SubscribeSuccessfully() } [Test] - public void SubscribingToRawBookUpdates_Should_TriggerWithRawBookUpdate() + public async Task SubscribingToRawBookUpdates_Should_TriggerWithRawBookUpdate() { // arrange var socket = new TestSocket(); @@ -291,19 +291,19 @@ public void SubscribingToRawBookUpdates_Should_TriggerWithRawBookUpdate() Precision = "R0", Length = 10 }; - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); BitfinexRawOrderBookEntry[] expected = new []{ new BitfinexRawOrderBookEntry()}; // act - socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); + await socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); // assert Assert.IsTrue(TestHelpers.AreEqual(result[0], expected[0])); } [Test] - public void SubscribingToTradeUpdates_Should_SubscribeSuccessfully() + public async Task SubscribingToTradeUpdates_Should_SubscribeSuccessfully() { // arrange var socket = new TestSocket(); @@ -322,7 +322,7 @@ public void SubscribingToTradeUpdates_Should_SubscribeSuccessfully() }; // act - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); @@ -331,7 +331,7 @@ public void SubscribingToTradeUpdates_Should_SubscribeSuccessfully() } [Test] - public void SubscribingToTradeUpdates_Should_TriggerWithTradeUpdate() + public async Task SubscribingToTradeUpdates_Should_TriggerWithTradeUpdate() { // arrange var socket = new TestSocket(); @@ -349,219 +349,219 @@ public void SubscribingToTradeUpdates_Should_TriggerWithTradeUpdate() Symbol = "BTCUSD", Pair = "BTCUSD" }; - socket.InvokeMessage(subResponse); + await socket.InvokeMessage(subResponse); subTask.Wait(5000); BitfinexTradeSimple[] expected = new[] { new BitfinexTradeSimple() }; // act - socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); + await socket.InvokeMessage($"[1, {JsonConvert.SerializeObject(expected)}]"); // assert Assert.IsTrue(TestHelpers.AreEqual(result[0], expected[0])); } - [TestCase("ou", BitfinexEventType.OrderUpdate)] - [TestCase("on", BitfinexEventType.OrderNew)] - [TestCase("oc", BitfinexEventType.OrderCancel)] - [TestCase("os", BitfinexEventType.OrderSnapshot, false)] - public void SubscribingToOrderUpdates_Should_TriggerWithOrderUpdate(string updateType, BitfinexEventType eventType, bool single=true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("ou", BitfinexEventType.OrderUpdate)] + //[TestCase("on", BitfinexEventType.OrderNew)] + //[TestCase("oc", BitfinexEventType.OrderCancel)] + //[TestCase("os", BitfinexEventType.OrderSnapshot, false)] + //public void SubscribingToOrderUpdates_Should_TriggerWithOrderUpdate(string updateType, BitfinexEventType eventType, bool single=true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent(eventType, new [] { new BitfinexOrder() { StatusString = "ACTIVE" }}); - client.SpotApi.SubscribeToUserTradeUpdatesAsync(data => - { - result = data.Data; - rstEvent.Set(); - }, null, null); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent(eventType, new [] { new BitfinexOrder() { StatusString = "ACTIVE" }}); + // client.SpotApi.SubscribeToUserTradeUpdatesAsync(data => + // { + // result = data.Data; + // rstEvent.Set(); + // }, null, null); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] {0, updateType, expected.Data[0] } : new object[] {0, updateType, new[] { expected.Data[0]} }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] {0, updateType, expected.Data[0] } : new object[] {0, updateType, new[] { expected.Data[0]} }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); + //} - [TestCase("te", BitfinexEventType.TradeExecuted)] - [TestCase("tu", BitfinexEventType.TradeExecutionUpdate)] - public void SubscribingToTradeUpdates_Should_TriggerWithTradeUpdate(string updateType, BitfinexEventType eventType, bool single = true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("te", BitfinexEventType.TradeExecuted)] + //[TestCase("tu", BitfinexEventType.TradeExecutionUpdate)] + //public void SubscribingToTradeUpdates_Should_TriggerWithTradeUpdate(string updateType, BitfinexEventType eventType, bool single = true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexTradeDetails() { } }); - client.SpotApi.SubscribeToUserTradeUpdatesAsync(null, - data => - { - result = data.Data; - rstEvent.Set(); - }, null); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexTradeDetails() { } }); + // client.SpotApi.SubscribeToUserTradeUpdatesAsync(null, + // data => + // { + // result = data.Data; + // rstEvent.Set(); + // }, null); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexTradeDetails() } : new object[] { 0, updateType, new[] { new BitfinexTradeDetails() } }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexTradeDetails() } : new object[] { 0, updateType, new[] { new BitfinexTradeDetails() } }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); + //} - [TestCase("ws", BitfinexEventType.WalletSnapshot, false)] - [TestCase("wu", BitfinexEventType.WalletUpdate)] - public void SubscribingToWalletUpdates_Should_TriggerWithWalletUpdate(string updateType, BitfinexEventType eventType, bool single = true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("ws", BitfinexEventType.WalletSnapshot, false)] + //[TestCase("wu", BitfinexEventType.WalletUpdate)] + //public void SubscribingToWalletUpdates_Should_TriggerWithWalletUpdate(string updateType, BitfinexEventType eventType, bool single = true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent>(eventType, new[] { new BitfinexWallet() { } }); - client.SpotApi.SubscribeToBalanceUpdatesAsync(data => - { - result = data.Data; - rstEvent.Set(); - }); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent>(eventType, new[] { new BitfinexWallet() { } }); + // client.SpotApi.SubscribeToBalanceUpdatesAsync(data => + // { + // result = data.Data; + // rstEvent.Set(); + // }); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexWallet() } : new object[] { 0, updateType, new[] { new BitfinexWallet() } }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexWallet() } : new object[] { 0, updateType, new[] { new BitfinexWallet() } }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data.First())); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data.First())); + //} - [TestCase("pn", BitfinexEventType.PositionNew)] - [TestCase("pc", BitfinexEventType.PositionClose)] - [TestCase("pu", BitfinexEventType.PositionUpdate)] - [TestCase("ps", BitfinexEventType.PositionSnapshot, false)] - public void SubscribingToPositionUpdates_Should_TriggerWithPositionUpdate(string updateType, BitfinexEventType eventType, bool single = true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("pn", BitfinexEventType.PositionNew)] + //[TestCase("pc", BitfinexEventType.PositionClose)] + //[TestCase("pu", BitfinexEventType.PositionUpdate)] + //[TestCase("ps", BitfinexEventType.PositionSnapshot, false)] + //public void SubscribingToPositionUpdates_Should_TriggerWithPositionUpdate(string updateType, BitfinexEventType eventType, bool single = true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent>(eventType, new[] { new BitfinexPosition() { } }); - client.SpotApi.SubscribeToUserTradeUpdatesAsync(null, null, data => - { - result = data.Data; - rstEvent.Set(); - }); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent>(eventType, new[] { new BitfinexPosition() { } }); + // client.SpotApi.SubscribeToUserTradeUpdatesAsync(null, null, data => + // { + // result = data.Data; + // rstEvent.Set(); + // }); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexPosition() } : new object[] { 0, updateType, new[] { new BitfinexPosition() } }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexPosition() } : new object[] { 0, updateType, new[] { new BitfinexPosition() } }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data.First())); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data.First())); + //} - [TestCase("fcn", BitfinexEventType.FundingCreditsNew)] - [TestCase("fcu", BitfinexEventType.FundingCreditsUpdate)] - [TestCase("fcc", BitfinexEventType.FundingCreditsClose)] - [TestCase("fcs", BitfinexEventType.FundingCreditsSnapshot, false)] - public void SubscribingToFundingCreditsUpdates_Should_TriggerWithFundingCreditsUpdate(string updateType, BitfinexEventType eventType, bool single = true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("fcn", BitfinexEventType.FundingCreditsNew)] + //[TestCase("fcu", BitfinexEventType.FundingCreditsUpdate)] + //[TestCase("fcc", BitfinexEventType.FundingCreditsClose)] + //[TestCase("fcs", BitfinexEventType.FundingCreditsSnapshot, false)] + //public void SubscribingToFundingCreditsUpdates_Should_TriggerWithFundingCreditsUpdate(string updateType, BitfinexEventType eventType, bool single = true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexFundingCredit() { StatusString="ACTIVE" } }); - client.SpotApi.SubscribeToFundingUpdatesAsync(null,data => - { - result = data.Data; - rstEvent.Set(); - }, null); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexFundingCredit() { StatusString="ACTIVE" } }); + // client.SpotApi.SubscribeToFundingUpdatesAsync(null,data => + // { + // result = data.Data; + // rstEvent.Set(); + // }, null); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexFundingCredit() { StatusString = "ACTIVE" } } : new object[] { 0, updateType, new[] { new BitfinexFundingCredit() { StatusString = "ACTIVE" } } }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexFundingCredit() { StatusString = "ACTIVE" } } : new object[] { 0, updateType, new[] { new BitfinexFundingCredit() { StatusString = "ACTIVE" } } }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); + //} - [TestCase("fln", BitfinexEventType.FundingLoanNew)] - [TestCase("flu", BitfinexEventType.FundingLoanUpdate)] - [TestCase("flc", BitfinexEventType.FundingLoanClose)] - [TestCase("fls", BitfinexEventType.FundingLoanSnapshot, false)] - public void SubscribingToFundingLoanUpdates_Should_TriggerWithFundingLoanUpdate(string updateType, BitfinexEventType eventType, bool single = true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("fln", BitfinexEventType.FundingLoanNew)] + //[TestCase("flu", BitfinexEventType.FundingLoanUpdate)] + //[TestCase("flc", BitfinexEventType.FundingLoanClose)] + //[TestCase("fls", BitfinexEventType.FundingLoanSnapshot, false)] + //public void SubscribingToFundingLoanUpdates_Should_TriggerWithFundingLoanUpdate(string updateType, BitfinexEventType eventType, bool single = true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexFunding() { StatusString = "ACTIVE" } }); - client.SpotApi.SubscribeToFundingUpdatesAsync(null, null, data => - { - result = data.Data; - rstEvent.Set(); - }); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexFunding() { StatusString = "ACTIVE" } }); + // client.SpotApi.SubscribeToFundingUpdatesAsync(null, null, data => + // { + // result = data.Data; + // rstEvent.Set(); + // }); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexFunding() { StatusString = "ACTIVE" } } : new object[] { 0, updateType, new[] { new BitfinexFunding() { StatusString = "ACTIVE" } } }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexFunding() { StatusString = "ACTIVE" } } : new object[] { 0, updateType, new[] { new BitfinexFunding() { StatusString = "ACTIVE" } } }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); + //} - [TestCase("fon", BitfinexEventType.FundingOfferNew)] - [TestCase("fou", BitfinexEventType.FundingOfferUpdate)] - [TestCase("foc", BitfinexEventType.FundingOfferCancel)] - [TestCase("fos", BitfinexEventType.FundingOfferSnapshot, false)] - public void SubscribingToFundingOfferUpdates_Should_TriggerWithFundingOfferUpdate(string updateType, BitfinexEventType eventType, bool single = true) - { - // arrange - var socket = new TestSocket(); - socket.CanConnect = true; - var client = TestHelpers.CreateAuthenticatedSocketClient(socket); + //[TestCase("fon", BitfinexEventType.FundingOfferNew)] + //[TestCase("fou", BitfinexEventType.FundingOfferUpdate)] + //[TestCase("foc", BitfinexEventType.FundingOfferCancel)] + //[TestCase("fos", BitfinexEventType.FundingOfferSnapshot, false)] + //public void SubscribingToFundingOfferUpdates_Should_TriggerWithFundingOfferUpdate(string updateType, BitfinexEventType eventType, bool single = true) + //{ + // // arrange + // var socket = new TestSocket(); + // socket.CanConnect = true; + // var client = TestHelpers.CreateAuthenticatedSocketClient(socket); - var rstEvent = new ManualResetEvent(false); - BitfinexSocketEvent> result = null; - var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexFundingOffer() { StatusString = "ACTIVE" } }); - client.SpotApi.SubscribeToFundingUpdatesAsync(data => - { - result = data.Data; - rstEvent.Set(); - }, null, null); + // var rstEvent = new ManualResetEvent(false); + // BitfinexSocketEvent> result = null; + // var expected = new BitfinexSocketEvent(eventType, new[] { new BitfinexFundingOffer() { StatusString = "ACTIVE" } }); + // client.SpotApi.SubscribeToFundingUpdatesAsync(data => + // { + // result = data.Data; + // rstEvent.Set(); + // }, null, null); - // act - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); - socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexFundingOffer() { StatusString = "ACTIVE" } } : new object[] { 0, updateType, new[] { new BitfinexFundingOffer() { StatusString = "ACTIVE" } } }); - rstEvent.WaitOne(1000); + // // act + // socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + // socket.InvokeMessage(single ? new object[] { 0, updateType, new BitfinexFundingOffer() { StatusString = "ACTIVE" } } : new object[] { 0, updateType, new[] { new BitfinexFundingOffer() { StatusString = "ACTIVE" } } }); + // rstEvent.WaitOne(1000); - // assert - Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); - } + // // assert + // Assert.IsTrue(TestHelpers.AreEqual(result.Data.First(), expected.Data[0])); + //} [Test] - public void PlacingAnOrder_Should_SucceedIfSuccessResponse() + public async Task PlacingAnOrder_Should_SucceedIfSuccessResponse() { // arrange var socket = new TestSocket(); @@ -580,9 +580,9 @@ public void PlacingAnOrder_Should_SucceedIfSuccessResponse() // act var placeTask = client.SpotApi.PlaceOrderAsync(OrderSide.Buy, OrderType.ExchangeLimit, "tBTCUSD", 1, price: 1, clientOrderId: 1234); - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + await socket.InvokeMessage(new BitfinexResponse() { Event = "auth", Status = "OK" }); Thread.Sleep(100); - socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); + await socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); var result = placeTask.Result; // assert @@ -591,7 +591,7 @@ public void PlacingAnOrder_Should_SucceedIfSuccessResponse() } [Test] - public void PlacingAnOrder_Should_FailIfErrorResponse() + public async Task PlacingAnOrder_Should_FailIfErrorResponse() { // arrange var socket = new TestSocket(); @@ -601,9 +601,9 @@ public void PlacingAnOrder_Should_FailIfErrorResponse() // act var placeTask = client.SpotApi.PlaceOrderAsync(OrderSide.Buy, OrderType.ExchangeLimit, "tBTCUSD", 1, price: 1, clientOrderId: 123); - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + await socket.InvokeMessage(new BitfinexResponse() { Event = "auth", Status = "OK" }); Thread.Sleep(100); - socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(order)}, 0, \"error\", \"order placing failed\"]]"); + await socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(order)}, 0, \"error\", \"order placing failed\"]]"); var result = placeTask.Result; // assert @@ -612,7 +612,7 @@ public void PlacingAnOrder_Should_FailIfErrorResponse() } [Test] - public void PlacingAnOrder_Should_FailIfNoResponse() + public async Task PlacingAnOrder_Should_FailIfNoResponse() { // arrange var socket = new TestSocket(); @@ -625,7 +625,7 @@ public void PlacingAnOrder_Should_FailIfNoResponse() // act var placeTask = client.SpotApi.PlaceOrderAsync(OrderSide.Buy, OrderType.ExchangeLimit, "tBTCUSD", 1, price: 1, clientOrderId: 123); - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + await socket.InvokeMessage(new BitfinexResponse() { Event = "auth", Message = "OK" }); var result = placeTask.Result; // assert @@ -633,7 +633,7 @@ public void PlacingAnOrder_Should_FailIfNoResponse() } [Test] - public void PlacingAnMarketOrder_Should_SucceedIfSuccessResponse() + public async Task PlacingAnMarketOrder_Should_SucceedIfSuccessResponse() { // arrange var socket = new TestSocket(); @@ -652,9 +652,9 @@ public void PlacingAnMarketOrder_Should_SucceedIfSuccessResponse() // act var placeTask = client.SpotApi.PlaceOrderAsync(OrderSide.Buy, OrderType.ExchangeMarket, "tBTCUSD", 1, price: 1, clientOrderId: 1234); - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + await socket.InvokeMessage(new BitfinexResponse() { Event = "auth", Status = "OK" }); Thread.Sleep(100); - socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); + await socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); var result = placeTask.Result; // assert @@ -663,7 +663,7 @@ public void PlacingAnMarketOrder_Should_SucceedIfSuccessResponse() } [Test] - public void PlacingAnFOKOrder_Should_SucceedIfSuccessResponse() + public async Task PlacingAnFOKOrder_Should_SucceedIfSuccessResponse() { // arrange var socket = new TestSocket(); @@ -682,9 +682,9 @@ public void PlacingAnFOKOrder_Should_SucceedIfSuccessResponse() // act var placeTask = client.SpotApi.PlaceOrderAsync(OrderSide.Buy, OrderType.ExchangeFillOrKill, "tBTCUSD", 1, price: 1, clientOrderId: 1234); - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + await socket.InvokeMessage(new BitfinexResponse() { Event = "auth", Status = "OK" }); Thread.Sleep(100); - socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); + await socket.InvokeMessage($"[0, \"n\", [0, \"on-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); var result = placeTask.Result; // assert @@ -693,7 +693,7 @@ public void PlacingAnFOKOrder_Should_SucceedIfSuccessResponse() } [Test] - public void PlacingAnFundingOffer_Should_SucceedIfSuccessResponse() + public async Task PlacingAnFundingOffer_Should_SucceedIfSuccessResponse() { // arrange var socket = new TestSocket(); @@ -712,9 +712,9 @@ public void PlacingAnFundingOffer_Should_SucceedIfSuccessResponse() // act var placeTask = client.SpotApi.SubmitFundingOfferAsync(FundingOfferType.Limit, "fUSD", 1, 1, 1); - socket.InvokeMessage(new BitfinexAuthenticationResponse() { Event = "auth", Status = "OK" }); + await socket.InvokeMessage(new BitfinexResponse() { Event = "auth", Status = "OK" }); Thread.Sleep(100); - socket.InvokeMessage($"[0, \"n\", [0, \"fon-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); + await socket.InvokeMessage($"[0, \"n\", [0, \"fon-req\", 0, 0, {JsonConvert.SerializeObject(expected)}, 0, \"SUCCESS\", \"Submitted\"]]"); var result = placeTask.Result; // assert @@ -724,7 +724,7 @@ public void PlacingAnFundingOffer_Should_SucceedIfSuccessResponse() [Test] - public void ReceivingAReconnectMessage_Should_ReconnectWebsocket() + public async Task ReceivingAReconnectMessage_Should_ReconnectWebsocket() { // arrange var socket = new TestSocket(); @@ -737,27 +737,25 @@ public void ReceivingAReconnectMessage_Should_ReconnectWebsocket() var rstEvent = new ManualResetEvent(false); var subTask = client.SpotApi.SubscribeToKlineUpdatesAsync("tBTCUSD", KlineInterval.FiveMinutes, data => { }); - socket.InvokeMessage(new CandleSubscriptionResponse() + await socket.InvokeMessage(new CandleSubscriptionResponse() { Channel = "candles", Event = "subscribed", ChannelId = 1, - Symbol = "tBTCUSD", Key = "trade:" + JsonConvert.SerializeObject(KlineInterval.FiveMinutes, new KlineIntervalConverter(false)) + ":tBTCUSD" }); - var subResult = subTask.Result; + var subResult = await subTask; subResult.Data.ConnectionRestored += (t) => rstEvent.Set(); // act - socket.InvokeMessage("{\"event\":\"info\", \"code\": 20051}"); + await socket.InvokeMessage("{\"event\":\"info\", \"code\": 20051}"); Thread.Sleep(100); - socket.InvokeMessage(new CandleSubscriptionResponse() + await socket.InvokeMessage(new CandleSubscriptionResponse() { Channel = "candles", Event = "subscribed", ChannelId = 1, - Symbol = "tBTCUSD", Key = "trade:" + JsonConvert.SerializeObject(KlineInterval.FiveMinutes, new KlineIntervalConverter(false)) + ":tBTCUSD" }); diff --git a/Bitfinex.Net.UnitTests/TestImplementations/TestHelpers.cs b/Bitfinex.Net.UnitTests/TestImplementations/TestHelpers.cs index cb14c8e..6772a8e 100644 --- a/Bitfinex.Net.UnitTests/TestImplementations/TestHelpers.cs +++ b/Bitfinex.Net.UnitTests/TestImplementations/TestHelpers.cs @@ -14,6 +14,7 @@ using Bitfinex.Net.Objects.Options; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; using Microsoft.Extensions.Logging; using Moq; diff --git a/Bitfinex.Net.UnitTests/TestImplementations/TestSocket.cs b/Bitfinex.Net.UnitTests/TestImplementations/TestSocket.cs index 89a8517..4495d57 100644 --- a/Bitfinex.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/Bitfinex.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 Binance.Net.UnitTests.TestImplementations { public class TestSocket: IWebsocket { - public bool CanConnect { get; set; } + public bool CanConnect { get; set; } = true; public bool Connected { get; set; } -#pragma warning disable 8618 - public event Action OnClose; - public event Action OnMessage; - public event Action OnError; - public event Action OnOpen; - public event Action OnReconnecting; - public event Action OnRequestSent; - public event Action OnReconnected; -#pragma warning restore 8618 + public event Func OnClose; +#pragma warning disable 0067 + 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; } @@ -98,14 +100,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) @@ -121,9 +125,9 @@ public async Task ProcessAsync() public async Task ReconnectAsync() { - OnReconnecting(); + await OnReconnecting(); await Task.Delay(10); - OnReconnected(); + await OnReconnected(); } } } diff --git a/Bitfinex.Net/Bitfinex.Net.csproj b/Bitfinex.Net/Bitfinex.Net.csproj index 395f2bc..cda1e13 100644 --- a/Bitfinex.Net/Bitfinex.Net.csproj +++ b/Bitfinex.Net/Bitfinex.Net.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netstandard2.1 enable @@ -7,9 +7,9 @@ Bitfinex.Net JKorf - 7.0.5 - 7.0.5 - 7.0.5 + 7.1.0-beta2 + 7.1.0 + 7.1.0 Bitfinex.Net is a .Net wrapper for the Bitfinex 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 Bitfinex Bitfinex.Net C# .Net CryptoCurrency Exchange API wrapper @@ -21,7 +21,7 @@ README.md en true - 7.0.5 - Updated CryptoExchange.Net + 7.1.0-beta2 - Fixed socket authentication Bitfinex.Net.xml @@ -51,8 +51,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Bitfinex.Net/Bitfinex.Net.xml b/Bitfinex.Net/Bitfinex.Net.xml index 6655e53..d19a2c9 100644 --- a/Bitfinex.Net/Bitfinex.Net.xml +++ b/Bitfinex.Net/Bitfinex.Net.xml @@ -33,39 +33,6 @@ - - - Helper functions - - - - - Add the IBitfinexClient and IBitfinexSocketClient 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 IBitfinexSocketClient for the service collection. Defaults to Singleton. - - - - - Validate the string is a valid Bitfinex symbol. - - string to validate - - - - Validate the string is a valid Bitfinex symbol. - - string to validate - - - - Validate the string is a valid Bitfinex symbol. - - string to validate - @@ -81,11 +48,6 @@ Option configuration delegate - - - Create a new instance of the BitfinexRestClient using provided options - - Create a new instance of the BitfinexRestClient using provided options @@ -494,43 +456,40 @@ - - - - + - + - + - + - + - + - + - + - + - + - + - + @@ -539,6 +498,9 @@ + + + @@ -560,22 +522,7 @@ - - - - - - - - - - - - - - - - + @@ -668,11 +615,6 @@ Multiple orders cancel request - - - Trade snapshot - - Trade executed @@ -693,11 +635,6 @@ Funding trade update - - - Historical order snapshot - - Margin info snapshot @@ -743,11 +680,6 @@ Funding offer cancel request - - - Historical funding offer snapshot - - Funding credits snapshot @@ -768,11 +700,6 @@ Funding credits closed - - - Historical funding credits snapshot - - Funding loan snapshot @@ -793,16 +720,6 @@ Funding loan closed - - - Historical funding loan snapshot - - - - - Historical funding trade snapshot - - Custom user price alert @@ -1423,6 +1340,29 @@ Deposit + + + Extension methods specific to using the Bitfinex API + + + + + Validate the string is a valid Bitfinex symbol. + + string to validate + + + + Validate the string is a valid Bitfinex symbol. + + string to validate + + + + Validate the string is a valid Bitfinex symbol. + + string to validate + General API endpoints @@ -2460,7 +2400,7 @@ Bitfinex spot streams - + Subscribes to ticker updates for a symbol. Use SubscribeToFundingTickerUpdatesAsync for funding symbol ticker updates @@ -2470,7 +2410,7 @@ Cancellation token for closing this subscription - + Subscribes to funding ticker updates a symbol. Use SubscribeToTickerUpdatesAsync for trade symbol ticker updates @@ -2480,7 +2420,7 @@ Cancellation token for closing this subscription - + Subscribes to order book updates for a symbol. Use SubscribeToFundingOrderBookUpdatesAsync for funding symbol ticker updates @@ -2494,7 +2434,7 @@ Cancellation token for closing this subscription - + Subscribes to funding order book updates for a symbol. Use SubscribeToOrderBookUpdatesAsync for trade symbol ticker updates @@ -2508,7 +2448,7 @@ Cancellation token for closing this subscription - + Subscribes to raw order book updates for a symbol. Use SubscribeToRawFundingOrderBookUpdatesAsync for funding symbol ticker updates @@ -2520,7 +2460,7 @@ Cancellation token for closing this subscription - + Subscribes to raw order book updates for a symbol. Use SubscribeToRawOrderBookUpdatesAsync for trade symbol ticker updates @@ -2532,7 +2472,7 @@ Cancellation token for closing this subscription - + Subscribes to public trade updates for a symbol @@ -2542,7 +2482,7 @@ Cancellation token for closing this subscription - + Subscribes to kline updates for a symbol @@ -2553,7 +2493,7 @@ Cancellation token for closing this subscription - + Subscribe to liquidation updates @@ -2562,7 +2502,7 @@ Cancellation token for closing this subscription - + Subscribe to derivatives status updates @@ -2572,7 +2512,7 @@ Cancellation token for closing this subscription - + Subscribe to trading information updates @@ -2580,28 +2520,15 @@ Data handler for order updates. Can be null if not interested Data handler for trade execution updates. Can be null if not interested Data handler for position updates. Can be null if not interested - Cancellation token for closing this subscription - - - - - Subscribe to wallet information updates - - - Data handler for wallet updates - Cancellation token for closing this subscription - - - - - Subscribe to funding information updates - - - - - Subscribe to funding offer updates. Can be null if not interested - Subscribe to funding credit updates. Can be null if not interested - Subscribe to funding loan updates. Can be null if not interested + Data handler for funding offer updates. Can be null if not interested + Data handler for funding credit updates. Can be null if not interested + Data handler for funding loan updates. Can be null if not interested + Data handler for wallet updates. Can be null if not interested + Data handler for balance updates. Can be null if not interested + Data handler for funding trade updates. Can be null if not interested + Data handler for funding info updates. Can be null if not interested + Data handler for margin base updates. Can be null if not interested + Data handler for margin symbol updates. Can be null if not interested Cancellation token for closing this subscription @@ -2626,6 +2553,13 @@ Affiliate code for the order + + + Cancel all orders + + + + Updates an order @@ -2852,6 +2786,21 @@ The quantity + + + Balance + + + + + Total Assets Under Management + + + + + Net Assets Under Management (total assets - total liabilities) + + Account change log @@ -4880,11 +4829,6 @@ Amount of time the funding transaction was for - - - The type of update - - Transfer info @@ -5412,6 +5356,12 @@ Spot API options + + + + + + Bitfinex order book factory @@ -5453,7 +5403,7 @@ - + Process a received checksum @@ -5474,5 +5424,39 @@ Dispose + + + Extensions for the ICryptoRestClient and ICryptoSocketClient interfaces + + + + + Get the Bitfinex REST Api client + + + + + + + Get the Bitfinex Websocket Api client + + + + + + + Extensions for DI + + + + + Add the IBitfinexClient and IBitfinexSocketClient 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 IBitfinexSocketClient for the service collection. Defaults to Singleton. + + diff --git a/Bitfinex.Net/BitfinexAuthenticationProvider.cs b/Bitfinex.Net/BitfinexAuthenticationProvider.cs index 0d9cc5c..7f4a3aa 100644 --- a/Bitfinex.Net/BitfinexAuthenticationProvider.cs +++ b/Bitfinex.Net/BitfinexAuthenticationProvider.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net.Http; -using System.Security.Cryptography; using System.Text; using Bitfinex.Net.Objects.Internal; using CryptoExchange.Net; diff --git a/Bitfinex.Net/Clients/BitfinexRestClient.cs b/Bitfinex.Net/Clients/BitfinexRestClient.cs index 8fdce4d..b551086 100644 --- a/Bitfinex.Net/Clients/BitfinexRestClient.cs +++ b/Bitfinex.Net/Clients/BitfinexRestClient.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using System.Net.Http; using Bitfinex.Net.Objects.Options; +using Microsoft.Extensions.DependencyInjection; namespace Bitfinex.Net.Clients { @@ -29,14 +30,7 @@ public class BitfinexRestClient : BaseRestClient, IBitfinexRestClient /// Create a new instance of the BitfinexRestClient using provided options /// /// Option configuration delegate - public BitfinexRestClient(Action optionsDelegate) : this(null, null, optionsDelegate) - { - } - - /// - /// Create a new instance of the BitfinexRestClient using provided options - /// - public BitfinexRestClient(ILoggerFactory? loggerFactory = null, HttpClient? httpClient = null) : this(httpClient, loggerFactory, null) + public BitfinexRestClient(Action? optionsDelegate = null) : this(null, null, optionsDelegate) { } diff --git a/Bitfinex.Net/Clients/BitfinexSocketClient.cs b/Bitfinex.Net/Clients/BitfinexSocketClient.cs index ae267a0..547e82a 100644 --- a/Bitfinex.Net/Clients/BitfinexSocketClient.cs +++ b/Bitfinex.Net/Clients/BitfinexSocketClient.cs @@ -1,17 +1,6 @@ -using Bitfinex.Net.Objects; -using CryptoExchange.Net; -using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using CryptoExchange.Net; using System; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Bitfinex.Net.Enums; -using System.Threading; -using Bitfinex.Net.Objects.Internal; using Bitfinex.Net.Interfaces.Clients; using Bitfinex.Net.Interfaces.Clients.SpotApi; using Bitfinex.Net.Clients.SpotApi; diff --git a/Bitfinex.Net/Clients/GeneralApi/BitfinexRestClientGeneralApiFunding.cs b/Bitfinex.Net/Clients/GeneralApi/BitfinexRestClientGeneralApiFunding.cs index 5aa41ed..8e24fc7 100644 --- a/Bitfinex.Net/Clients/GeneralApi/BitfinexRestClientGeneralApiFunding.cs +++ b/Bitfinex.Net/Clients/GeneralApi/BitfinexRestClientGeneralApiFunding.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Bitfinex.Net.Objects.Models; using Bitfinex.Net.Interfaces.Clients.GeneralApi; +using Bitfinex.Net.ExtensionMethods; namespace Bitfinex.Net.Clients.GeneralApi { diff --git a/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiAccount.cs b/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiAccount.cs index 1f2e9af..578d548 100644 --- a/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiAccount.cs +++ b/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiAccount.cs @@ -14,6 +14,7 @@ using Bitfinex.Net.Objects.Models; using Bitfinex.Net.Objects.Models.V1; using Bitfinex.Net.Interfaces.Clients.SpotApi; +using Bitfinex.Net.ExtensionMethods; namespace Bitfinex.Net.Clients.SpotApi { diff --git a/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiExchangeData.cs b/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiExchangeData.cs index 02bd41c..88fb6c2 100644 --- a/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiExchangeData.cs +++ b/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiExchangeData.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Bitfinex.Net.Objects.Models; using Bitfinex.Net.Interfaces.Clients.SpotApi; +using Bitfinex.Net.ExtensionMethods; namespace Bitfinex.Net.Clients.SpotApi { diff --git a/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiTrading.cs b/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiTrading.cs index a4f81e4..33c0b36 100644 --- a/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiTrading.cs +++ b/Bitfinex.Net/Clients/SpotApi/BitfinexRestClientSpotApiTrading.cs @@ -13,9 +13,9 @@ using System.Threading; using System.Threading.Tasks; using Bitfinex.Net.Objects.Models; -using Bitfinex.Net.Objects.Models.V1; using Bitfinex.Net.Interfaces.Clients.SpotApi; using CryptoExchange.Net.CommonObjects; +using Bitfinex.Net.ExtensionMethods; namespace Bitfinex.Net.Clients.SpotApi { diff --git a/Bitfinex.Net/Clients/SpotApi/BitfinexSocketClientSpotApi.cs b/Bitfinex.Net/Clients/SpotApi/BitfinexSocketClientSpotApi.cs index 4c0f1ba..32961ac 100644 --- a/Bitfinex.Net/Clients/SpotApi/BitfinexSocketClientSpotApi.cs +++ b/Bitfinex.Net/Clients/SpotApi/BitfinexSocketClientSpotApi.cs @@ -1,30 +1,45 @@ using Bitfinex.Net.Converters; using CryptoExchange.Net; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using CryptoExchange.Net.Authentication; -using Bitfinex.Net.Enums; using System.Threading; using Bitfinex.Net.Objects.Internal; -using Bitfinex.Net.Objects.Models; using Bitfinex.Net.Objects.Models.Socket; using Bitfinex.Net.Interfaces.Clients.SpotApi; using Bitfinex.Net.Objects.Options; using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Objects.Sockets; +using Bitfinex.Net.Objects.Sockets.Subscriptions; +using Bitfinex.Net.Objects.Models; +using Bitfinex.Net.Enums; +using System.Collections.Generic; +using System.Linq; +using CryptoExchange.Net.Sockets; +using System.Globalization; +using Bitfinex.Net.Objects.Sockets.Queries; +using CryptoExchange.Net.Sockets.MessageParsing; +using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using Bitfinex.Net.ExtensionMethods; namespace Bitfinex.Net.Clients.SpotApi { /// public class BitfinexSocketClientSpotApi : SocketApiClient, IBitfinexSocketClientSpotApi { + private static readonly MessagePath _0Path = MessagePath.Get().Index(0); + private static readonly MessagePath _eventPath = MessagePath.Get().Property("event"); + private static readonly MessagePath _channelPath = MessagePath.Get().Property("channel"); + private static readonly MessagePath _symbolPath = MessagePath.Get().Property("symbol"); + private static readonly MessagePath _precPath = MessagePath.Get().Property("prec"); + private static readonly MessagePath _freqPath = MessagePath.Get().Property("freq"); + private static readonly MessagePath _lenPath = MessagePath.Get().Property("len"); + private static readonly MessagePath _keyPath = MessagePath.Get().Property("key"); + private static readonly MessagePath _chanIdPath = MessagePath.Get().Property("chanId"); + #region fields private readonly JsonSerializer _bookSerializer = new JsonSerializer(); private readonly JsonSerializer _fundingBookSerializer = new JsonSerializer(); @@ -33,50 +48,60 @@ public class BitfinexSocketClientSpotApi : SocketApiClient, IBitfinexSocketClien /// public new BitfinexSocketOptions ClientOptions => (BitfinexSocketOptions)base.ClientOptions; + #endregion #region ctor internal BitfinexSocketClientSpotApi(ILogger logger, BitfinexSocketOptions options) : base(logger, options.Environment.SocketAddress, options, options.SpotOptions) { - ContinueOnQueryResponse = true; UnhandledMessageExpected = true; - AddGenericHandler("HB", (messageEvent) => { }); - AddGenericHandler("Info", InfoHandler); - AddGenericHandler("Conf", ConfHandler); + AddSystemSubscription(new BitfinexInfoSubscription(_logger)); _affCode = options.AffiliateCode; _bookSerializer.Converters.Add(new OrderBookEntryConverter()); _fundingBookSerializer.Converters.Add(new OrderBookFundingEntryConverter()); } #endregion + /// protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => new BitfinexAuthenticationProvider(credentials, ClientOptions.NonceProvider ?? new BitfinexNonceProvider()); - #region public methods + /// + protected override Query GetAuthenticationRequest() + { + var authProvider = (BitfinexAuthenticationProvider)AuthenticationProvider!; + var n = authProvider.GetNonce().ToString(); + var authentication = new BitfinexAuthentication + { + Event = "auth", + ApiKey = authProvider.GetApiKey(), + Nonce = n, + Payload = "AUTH" + n + }; + + authentication.Signature = authProvider.Sign(authentication.Payload).ToLower(CultureInfo.InvariantCulture); + return new BitfinexAuthQuery(authentication); + } /// public async Task> SubscribeToTickerUpdatesAsync(string symbol, Action> handler, CancellationToken ct = default) { symbol.ValidateBitfinexTradingSymbol(); - var internalHandler = new Action>(data => - { - HandleData("Ticker", (JArray)data.Data[1]!, symbol, data, handler); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexSubscriptionRequest("ticker", symbol), null, false, internalHandler, ct).ConfigureAwait(false); + + var subscription = new BitfinexSubscription(_logger, "ticker", symbol, x => handler(x.As(x.Data.First()))); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToFundingTickerUpdatesAsync(string symbol, Action> handler, CancellationToken ct = default) { symbol.ValidateBitfinexFundingSymbol(); - var internalHandler = new Action>(data => - { - HandleData("Ticker", (JArray)data.Data[1]!, symbol, data, handler); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexSubscriptionRequest("ticker", symbol), null, false, internalHandler, ct).ConfigureAwait(false); + + var subscription = new BitfinexSubscription(_logger, "ticker", symbol, x => handler(x.As(x.Data.First()))); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// @@ -87,37 +112,8 @@ public async Task> SubscribeToOrderBookUpdatesAsy if (precision == Precision.R0) throw new ArgumentException("Invalid precision R0, use SubscribeToRawBookUpdatesAsync instead"); - var internalHandler = new Action>(data => - { - if (data.Data[1]?.ToString() == "cs") - { - // Process - checksumHandler?.Invoke(data.As(data.Data[2]!.Value(), symbol)); - } - else - { - var dataArray = (JArray)data.Data[1]!; - if (dataArray.Count == 0) - // Empty array - return; - - if (dataArray[0].Type == JTokenType.Array) - { - HandleData("Book snapshot", dataArray, symbol, data, handler, _bookSerializer); - } - else - { - HandleSingleToArrayData("Book update", dataArray, symbol, data, handler, _bookSerializer); - } - } - }); - - var sub = new BitfinexBookSubscriptionRequest( - symbol, - JsonConvert.SerializeObject(precision, new PrecisionConverter(false)), - JsonConvert.SerializeObject(frequency, new FrequencyConverter(false)), - length); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), sub, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexSubscription(_logger, "book", symbol, handler, checksumHandler, false, precision, frequency, length); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// @@ -128,193 +124,74 @@ public async Task> SubscribeToFundingOrderBookUpd if (precision == Precision.R0) throw new ArgumentException("Invalid precision R0, use SubscribeToFundingRawOrderBookUpdatesAsync instead"); - var internalHandler = new Action>(data => - { - if (data.Data[1]?.ToString() == "cs") - { - // Process - checksumHandler?.Invoke(data.As(data.Data[2]!.Value(), symbol)); - } - else - { - var dataArray = (JArray)data.Data[1]!; - if (dataArray.Count == 0) - // Empty array - return; - - if (dataArray[0].Type == JTokenType.Array) - { - HandleData("Book snapshot", dataArray, symbol, data, handler, _fundingBookSerializer); - } - else - { - HandleSingleToArrayData("Book update", dataArray, symbol, data, handler, _fundingBookSerializer); - } - } - }); - - var sub = new BitfinexBookSubscriptionRequest( - symbol, - JsonConvert.SerializeObject(precision, new PrecisionConverter(false)), - JsonConvert.SerializeObject(frequency, new FrequencyConverter(false)), - length); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), sub, null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexSubscription(_logger, "book", symbol, handler, checksumHandler, false, precision, frequency, length); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToRawOrderBookUpdatesAsync(string symbol, int limit, Action>> handler, Action>? checksumHandler = null, CancellationToken ct = default) { symbol.ValidateBitfinexTradingSymbol(); - var internalHandler = new Action>(data => - { - if (data.Data[1]?.ToString() == "cs") - { - // Process - checksumHandler?.Invoke(data.As(data.Data[2]!.Value(), symbol)); - } - else - { - var dataArray = (JArray)data.Data[1]!; - if (dataArray[0].Type == JTokenType.Array) - HandleData("Raw book snapshot", dataArray, symbol, data, handler); - else - HandleSingleToArrayData("Raw book update", dataArray, symbol, data, handler); - } - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexRawBookSubscriptionRequest(symbol, "R0", limit), null, false, internalHandler, ct).ConfigureAwait(false); + + var subscription = new BitfinexSubscription(_logger, "book", symbol, handler, checksumHandler, false, Precision.R0, Frequency.Realtime, limit); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToRawFundingOrderBookUpdatesAsync(string symbol, int limit, Action>> handler, Action>? checksumHandler = null, CancellationToken ct = default) { symbol.ValidateBitfinexFundingSymbol(); - var internalHandler = new Action>(data => - { - if (data.Data[1]?.ToString() == "cs") - { - // Process - checksumHandler?.Invoke(data.As(data.Data[2]!.Value(), symbol)); - } - else - { - var dataArray = (JArray)data.Data[1]!; - if (dataArray[0].Type == JTokenType.Array) - HandleData("Raw book snapshot", dataArray, symbol, data, handler); - else - HandleSingleToArrayData("Raw book update", dataArray, symbol, data, handler); - } - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexRawBookSubscriptionRequest(symbol, "R0", limit), null, false, internalHandler, ct).ConfigureAwait(false); + + var subscription = new BitfinexSubscription(_logger, "book", symbol, handler, checksumHandler, false, Precision.R0, Frequency.Realtime, limit); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToTradeUpdatesAsync(string symbol, Action>> handler, CancellationToken ct = default) { - var internalHandler = new Action>(data => - { - var arr = (JArray)data.Data; - if (arr[1].Type == JTokenType.Array) - { - HandleData("Trade snapshot", (JArray)arr[1], symbol, data, handler); - } - else - { - var desResult = Deserialize(arr[2]); - if (!desResult) - { - _logger.Log(LogLevel.Warning, "Failed to deserialize trade object: " + desResult.Error); - return; - } - desResult.Data.UpdateType = BitfinexEvents.EventMapping[arr[1].ToString()]; - handler(data.As>(new[] { desResult.Data }, symbol)); - } - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexSubscriptionRequest("trades", symbol), null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexSubscription(_logger, "trades", symbol, handler); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToKlineUpdatesAsync(string symbol, KlineInterval interval, Action>> handler, CancellationToken ct = default) { - var internalHandler = new Action>(data => - { - var dataArray = (JArray)data.Data[1]!; - if (dataArray.Count == 0) - { - _logger.Log(LogLevel.Warning, "No data in kline update, check if the symbol is correct"); - return; - } - - if (dataArray[0].Type == JTokenType.Array) - HandleData("Kline snapshot", dataArray, symbol, data, handler); - else - HandleSingleToArrayData("Kline update", dataArray, symbol, data, handler); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexKlineSubscriptionRequest(symbol, JsonConvert.SerializeObject(interval, new KlineIntervalConverter(false))), null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexSubscription(_logger, "candles", null, handler, key: $"trade:{JsonConvert.SerializeObject(interval, new KlineIntervalConverter(false))}:" + symbol); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToLiquidationUpdatesAsync(Action>> handler, CancellationToken ct = default) { - var internalHandler = new Action>(data => - { - HandleData("Liquidation", (JArray)data.Data[1]!, null, data, handler); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexLiquidationSubscriptionRequest(), null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexSubscription(_logger, "status", null, handler, key: $"liq:global"); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// public async Task> SubscribeToDerivativesUpdatesAsync(string symbol, Action> handler, CancellationToken ct = default) { - var internalHandler = new Action>(data => - { - HandleData("DerivativeUpdate", (JArray)data.Data[1]!, symbol, data, handler); - }); - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), new BitfinexDerivativesStatusSubscriptionRequest(symbol), null, false, internalHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexSubscription(_logger, "candles", null, x => handler(x.As(x.Data.Single())), key: $"deriv:" + symbol); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// - public async Task> SubscribeToUserTradeUpdatesAsync( - Action>>> orderHandler, - Action>>> tradeHandler, - Action>>> positionHandler, + public async Task> SubscribeToUserUpdatesAsync( + Action>>? orderHandler = null, + Action>>? positionHandler = null, + Action>>? fundingOfferHandler = null, + Action>>? fundingCreditHandler = null, + Action>>? fundingLoanHandler = null, + Action>>? walletHandler = null, + Action>? balanceHandler = null, + Action>? tradeHandler = null, + Action>? fundingTradeHandler = null, + Action>? fundingInfoHandler = null, + Action>? marginBaseHandler = null, + Action>? marginSymbolHandler = null, CancellationToken ct = default) { - var tokenHandler = new Action>(tokenData => - { - HandleAuthUpdate(tokenData, orderHandler, "Orders"); - HandleAuthUpdate(tokenData, tradeHandler, "Trades"); - HandleAuthUpdate(tokenData, positionHandler, "Positions"); - }); - - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), null, "Orders|Trades|Positions", true, tokenHandler, ct).ConfigureAwait(false); - } - - /// - public async Task> SubscribeToBalanceUpdatesAsync(Action>>> walletHandler, CancellationToken ct = default) - { - var tokenHandler = new Action>(tokenData => - { - HandleAuthUpdate(tokenData, walletHandler, "Wallet"); - }); - - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), null, "Wallet", true, tokenHandler, ct).ConfigureAwait(false); - } - - /// - public async Task> SubscribeToFundingUpdatesAsync( - Action>>> fundingOfferHandler, - Action>>> fundingCreditHandler, - Action>>> fundingLoanHandler, - CancellationToken ct = default) - { - var tokenHandler = new Action>(tokenData => - { - HandleAuthUpdate(tokenData, fundingOfferHandler, "FundingOffers"); - HandleAuthUpdate(tokenData, fundingCreditHandler, "FundingCredits"); - HandleAuthUpdate(tokenData, fundingLoanHandler, "FundingLoans"); - }); - - return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), null, "FundingOffers|FundingCredits|FundingLoans", true, tokenHandler, ct).ConfigureAwait(false); + var subscription = new BitfinexUserSubscription(_logger, positionHandler, walletHandler, orderHandler, fundingOfferHandler, fundingCreditHandler, fundingLoanHandler, balanceHandler, tradeHandler, fundingTradeHandler, fundingInfoHandler, marginBaseHandler, marginSymbolHandler); + return await SubscribeAsync(BaseAddress.AppendPath("ws/2"), subscription, ct).ConfigureAwait(false); } /// @@ -342,7 +219,9 @@ public async Task> PlaceOrderAsync(OrderSide side, Ord Meta = affCode == null ? null : new BitfinexMeta() { AffiliateCode = affCode } }); - return await QueryAsync(BaseAddress.AppendPath("ws/2"), query, true).ConfigureAwait(false); + var bitfinexQuery = new BitfinexQuery(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As(result.Data?.Data.Data); } /// @@ -359,120 +238,56 @@ public async Task> UpdateOrderAsync(long orderId, deci PriceTrailing = priceTrailing?.ToString(CultureInfo.InvariantCulture) }); - return await QueryAsync(BaseAddress.AppendPath("ws/2"), query, true).ConfigureAwait(false); + var bitfinexQuery = new BitfinexQuery(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As(result.Data?.Data.Data); } - ///// - ///// Cancel all open orders - ///// - ///// - //public CallResult CancelAllOrders() => CancelAllOrdersAsync().Result; - ///// - ///// Cancel all open orders - ///// - ///// - //public async Task> CancelAllOrdersAsync() - //{ - // // Doesn't seem to work even though it is implemented as described at https://docs.bitfinex.com/v2/reference#ws-input-order-cancel-multi - // _log.Write(LogLevel.Information, "Going to cancel all orders"); - // var query = new BitfinexSocketQuery(null, BitfinexEventType.OrderCancelMulti, new BitfinexMultiCancel { All = true }); - - // return await Query(query, true).ConfigureAwait(false); - //} + /// + public async Task>> CancelAllOrdersAsync() + { + var query = new BitfinexSocketQuery(null, BitfinexEventType.OrderCancelMulti, new BitfinexMultiCancel { All = true }); + var bitfinexQuery = new BitfinexQuery>(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As>(result.Data?.Data.Data); + } /// public async Task> CancelOrderAsync(long orderId) { - _logger.Log(LogLevel.Information, "Going to cancel order " + orderId); - var query = new BitfinexSocketQuery(orderId.ToString(CultureInfo.InvariantCulture), BitfinexEventType.OrderCancel, new JObject { ["id"] = orderId }); - - return await QueryAsync(BaseAddress.AppendPath("ws/2"), query, true).ConfigureAwait(false); + var query = new BitfinexSocketQuery(orderId.ToString(CultureInfo.InvariantCulture), BitfinexEventType.OrderCancel, new Dictionary { ["id"] = orderId }); + var bitfinexQuery = new BitfinexQuery(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As(result.Data?.Data.Data); } /// - public async Task> CancelOrdersByGroupIdAsync(long groupOrderId) + public async Task>> CancelOrdersByGroupIdAsync(long groupOrderId) { return await CancelOrdersAsync(null, null, new Dictionary { { groupOrderId, null } }).ConfigureAwait(false); } /// - public async Task> CancelOrdersByGroupIdsAsync(IEnumerable groupOrderIds) + public async Task>> CancelOrdersByGroupIdsAsync(IEnumerable groupOrderIds) { groupOrderIds.ValidateNotNull(nameof(groupOrderIds)); return await CancelOrdersAsync(null, null, groupOrderIds.ToDictionary(v => v, k => (long?)null)).ConfigureAwait(false); } /// - public async Task> CancelOrdersAsync(IEnumerable orderIds) + public async Task>> CancelOrdersAsync(IEnumerable orderIds) { orderIds.ValidateNotNull(nameof(orderIds)); return await CancelOrdersAsync(orderIds, null).ConfigureAwait(false); } /// - public async Task> CancelOrdersByClientOrderIdsAsync(Dictionary clientOrderIds) + public async Task>> CancelOrdersByClientOrderIdsAsync(Dictionary clientOrderIds) { return await CancelOrdersAsync(null, clientOrderIds).ConfigureAwait(false); } - /// - public async Task> SubmitFundingOfferAsync(FundingOfferType type, string symbol, decimal quantity, decimal price, int period, int? flags = null) - { - var parameters = new Dictionary - { - { "type", EnumConverter.GetString(type) }, - { "symbol", symbol }, - { "amount", quantity }, - { "rate", price }, - { "period", period }, - }; - parameters.AddOptionalParameter("flags", flags); - - var query = new BitfinexSocketQuery(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), BitfinexEventType.FundingOfferNew, parameters); - return await QueryAsync(BaseAddress.AppendPath("ws/2"), query, true).ConfigureAwait(false); - } - - /// - public async Task> CancelFundingOfferAsync(long id) - { - var parameters = new Dictionary - { - { "id", id } - }; - - var query = new BitfinexSocketQuery(id.ToString(CultureInfo.InvariantCulture), BitfinexEventType.FundingOfferCancel, parameters); - return await QueryAsync(BaseAddress.AppendPath("ws/2"), query, true).ConfigureAwait(false); - } - #endregion - - #region private methods - private void HandleData(string name, JArray dataArray, string? symbol, DataEvent dataEvent, Action> handler, JsonSerializer? serializer = null) - { - var desResult = Deserialize(dataArray, serializer: serializer); - if (!desResult) - { - _logger.Log(LogLevel.Warning, $"Failed to Deserialize {name} object: " + desResult.Error); - return; - } - - handler(dataEvent.As(desResult.Data, symbol)); - } - - private void HandleSingleToArrayData(string name, JArray dataArray, string? symbol, DataEvent dataEvent, Action>> handler, JsonSerializer? serializer = null) - { - var wrapperArray = new JArray { dataArray }; - - var desResult = Deserialize>(wrapperArray, serializer: serializer); - if (!desResult) - { - _logger.Log(LogLevel.Warning, $"Failed to Deserialize {name} object: " + desResult.Error); - return; - } - - handler(dataEvent.As(desResult.Data, symbol)); - } - - private async Task> CancelOrdersAsync(IEnumerable? orderIds = null, Dictionary? clientOrderIds = null, Dictionary? groupOrderIds = null) + private async Task>> CancelOrdersAsync(IEnumerable? orderIds = null, Dictionary? clientOrderIds = null, Dictionary? groupOrderIds = null) { if (orderIds == null && clientOrderIds == null && groupOrderIds == null) throw new ArgumentException("Either orderIds, clientOrderIds or groupOrderIds should be provided"); @@ -495,466 +310,70 @@ private async Task> CancelOrdersAsync(IEnumerable? orderI cancelObject.GroupIds = new[] { groupOrderIds.Select(g => g.Key).ToArray() }; var query = new BitfinexSocketQuery(null, BitfinexEventType.OrderCancelMulti, cancelObject); - return await QueryAsync(query, true).ConfigureAwait(false); + var bitfinexQuery = new BitfinexQuery>(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As>(result.Data?.Data.Data); } - private void HandleAuthUpdate(DataEvent token, Action>>> action, string category) - { - var evntStr = token.Data[1]?.ToString(); - if (evntStr == null) - return; - - var evntType = BitfinexEvents.EventMapping[evntStr]; - var evnt = BitfinexEvents.Events.Single(e => e.EventType == evntType); - if (evnt.Category != category) - return; - - if (action == null) - { - _logger.Log(LogLevel.Debug, $"Ignoring {evnt.EventType} event because not subscribed"); - return; - } - - IEnumerable data; - if (evnt.Single) - { - var result = Deserialize(token.Data[2]!); - if (!result) - { - _logger.Log(LogLevel.Warning, "Failed to Deserialize data: " + result.Error); - return; - } - data = new[] { result.Data }; - } - else - { - var result = Deserialize>(token.Data[2]!); - if (!result) - { - _logger.Log(LogLevel.Warning, "Failed to Deserialize data: " + result.Error); - return; - } - data = result.Data; - } - - action(token.As(new BitfinexSocketEvent>(evntType, data))); - } - - private long GenerateClientOrderId() - { - var buffer = new byte[8]; - _random.NextBytes(buffer); - return (long)Math.Round(Math.Abs(BitConverter.ToInt32(buffer, 0)) / 1000m); - } - - - private void ConfHandler(MessageEvent messageEvent) - { - var confEvent = messageEvent.JsonData.Type == JTokenType.Object && messageEvent.JsonData["event"]?.ToString() == "conf"; - if (!confEvent) - return; - - // Could check conf result; - } - - private void InfoHandler(MessageEvent messageEvent) - { - var infoEvent = messageEvent.JsonData.Type == JTokenType.Object && messageEvent.JsonData["event"]?.ToString() == "info"; - if (!infoEvent) - return; - - _logger.Log(LogLevel.Debug, $"Socket {messageEvent.Connection.SocketId} Info event received: {messageEvent.JsonData}"); - if (messageEvent.JsonData["code"] == null) - { - // welcome event, send a config message for receiving checsum updates for order book subscriptions - messageEvent.Connection.Send(ExchangeHelpers.NextId(), new BitfinexSocketConfig { Event = "conf", Flags = 131072 }, 1); - return; - } - - var code = messageEvent.JsonData["code"]?.Value(); - switch (code) - { - case 20051: - _logger.Log(LogLevel.Information, $"Socket {messageEvent.Connection.SocketId} Code {code} received, reconnecting socket"); - messageEvent.Connection.PausedActivity = true; // Prevent new operations to be send - _ = messageEvent.Connection.TriggerReconnectAsync(); - break; - case 20060: - _logger.Log(LogLevel.Information, $"Socket {messageEvent.Connection.SocketId} Code {code} received, entering maintenance mode"); - messageEvent.Connection.PausedActivity = true; - break; - case 20061: - _logger.Log(LogLevel.Information, $"Socket {messageEvent.Connection.SocketId} Code {code} received, leaving maintenance mode. Reconnecting/Resubscribing socket."); - _ = messageEvent.Connection.TriggerReconnectAsync(); // Closing it via socket will automatically reconnect - break; - default: - _logger.Log(LogLevel.Warning, $"Socket {messageEvent.Connection.SocketId} Unknown info code received: {code}"); - break; - } - } - - /// - protected override async Task UnsubscribeAsync(SocketConnection connection, SocketSubscription subscription) - { - if (subscription.Request == null) - { - // If we don't have a request object we can't unsubscribe it. Probably is an auth subscription which gets pushed regardless - // Just returning true here will remove the handler and close the socket if there are no other handlers left on the socket, which is the best we can do - return true; - } - - var channelId = ((BitfinexSubscriptionRequest)subscription.Request!).ChannelId; - var unsub = new BitfinexUnsubscribeRequest(channelId); - var result = false; - await connection.SendAndWaitAsync(unsub, ClientOptions.RequestTimeout, null, 1, data => - { - if (data.Type != JTokenType.Object) - return false; - - var evnt = data["event"]?.ToString(); - var channel = data["chanId"]?.ToString(); - if (evnt == null || channel == null) - return false; - - if (!int.TryParse(channel, out var chan)) - return false; - - result = evnt == "unsubscribed" && channelId == chan; - return result; - }).ConfigureAwait(false); - return result; - } - - private static BitfinexAuthentication GetAuthObject(SocketApiClient apiClient, params string[] filter) + public async Task> SubmitFundingOfferAsync(FundingOfferType type, string symbol, decimal quantity, decimal price, int period, int? flags = null) { - var authProvider = (BitfinexAuthenticationProvider)apiClient.AuthenticationProvider!; - var n = authProvider.GetNonce().ToString(); - var authentication = new BitfinexAuthentication + var parameters = new Dictionary { - Event = "auth", - ApiKey = authProvider.GetApiKey(), - Nonce = n, - Payload = "AUTH" + n + { "type", EnumConverter.GetString(type) }, + { "symbol", symbol }, + { "amount", quantity }, + { "rate", price }, + { "period", period }, }; - if (filter.Any()) - authentication.Filter = filter; - authentication.Signature = authProvider.Sign(authentication.Payload).ToLower(CultureInfo.InvariantCulture); - return authentication; - } - - #endregion - - /// - protected override async Task> AuthenticateSocketAsync(SocketConnection s) - { - if (s.ApiClient.AuthenticationProvider == null) - return new CallResult(new NoApiCredentialsError()); - - var authObject = GetAuthObject(s.ApiClient); - var result = new CallResult(new ServerError("No response from server")); - await s.SendAndWaitAsync(authObject, ClientOptions.RequestTimeout, null, 1, tokenData => - { - if (tokenData.Type != JTokenType.Object) - return false; - - if (tokenData["event"]?.ToString() != "auth") - return false; - - var authResponse = Deserialize(tokenData); - if (!authResponse) - { - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} authentication failed: " + authResponse.Error); - result = new CallResult(authResponse.Error!); - return false; - } - - if (authResponse.Data.Status != "OK") - { - var error = new ServerError(authResponse.Data.ErrorCode, authResponse.Data.ErrorMessage ?? "-"); - result = new CallResult(error); - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} authentication failed: " + error); - return false; - } - - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} authentication completed"); - result = new CallResult(true); - return true; - }).ConfigureAwait(false); - - return result; - } - - /// -#pragma warning disable 8765 - protected override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult? callResult) -#pragma warning restore 8765 - { - callResult = null; - if (data.Type != JTokenType.Array) - return false; - - var array = (JArray)data; - if (array.Count < 3) - return false; - - var bfRequest = (BitfinexSocketQuery)request; - var evntString = data[1]!.ToString(); - if (!BitfinexEvents.EventMapping.TryGetValue(evntString, out var eventType)) - return false; - - if (eventType == BitfinexEventType.Notification) - { - var notificationData = (JArray)data[2]!; - var notificationType = BitfinexEvents.EventMapping[notificationData[1].ToString()]; - if (notificationType != BitfinexEventType.OrderNewRequest - && notificationType != BitfinexEventType.OrderCancelRequest - && notificationType != BitfinexEventType.OrderUpdateRequest - && notificationType != BitfinexEventType.OrderCancelMultiRequest - && notificationType != BitfinexEventType.FundingOfferNewRequest - && notificationType != BitfinexEventType.FundingOfferCancelRequest) - { - return false; - } - - var statusString = (notificationData[6].ToString()).ToLower(CultureInfo.InvariantCulture); - if (statusString == "error") - { - if (bfRequest.QueryType == BitfinexEventType.OrderNew && notificationType == BitfinexEventType.OrderNewRequest) - { - var orderData = notificationData[4]; - if (orderData[2]?.ToString() != bfRequest.Id) - return false; - - callResult = new CallResult(new ServerError(notificationData[7].ToString())); - return true; - } - - if (bfRequest.QueryType == BitfinexEventType.OrderCancel && notificationType == BitfinexEventType.OrderCancelRequest) - { - var orderData = notificationData[4]; - if (orderData[0]?.ToString() != bfRequest.Id) - return false; - - callResult = new CallResult(new ServerError(notificationData[7].ToString())); - return true; - } - - if (bfRequest.QueryType == BitfinexEventType.OrderUpdate && notificationType == BitfinexEventType.OrderUpdateRequest) - { - // OrderUpdateRequest not found notification doesn't carry the order id, where as OrderCancelRequest not found notification does.. - // Anyway, can't check for ids, so just assume its for this one - - callResult = new CallResult(new ServerError(notificationData[7].ToString())); - return true; - } - - if (bfRequest.QueryType == BitfinexEventType.OrderCancelMulti && notificationType == BitfinexEventType.OrderCancelMultiRequest) - { - callResult = new CallResult(new ServerError(notificationData[7].ToString())); - return true; - } - - if (bfRequest.QueryType == BitfinexEventType.FundingOfferNew && notificationType == BitfinexEventType.FundingOfferNewRequest) - { - callResult = new CallResult(new ServerError(notificationData[7].ToString())); - return true; - } - - if (bfRequest.QueryType == BitfinexEventType.FundingOfferCancel && notificationType == BitfinexEventType.FundingOfferCancelRequest) - { - var fundingData = notificationData[4]; - if (fundingData[0]?.ToString() != bfRequest.Id) - return false; - - callResult = new CallResult(new ServerError(notificationData[7].ToString())); - return true; - } - } - - if (notificationType == BitfinexEventType.OrderNewRequest - || notificationType == BitfinexEventType.OrderUpdateRequest - || notificationType == BitfinexEventType.OrderCancelRequest) - { - if (bfRequest.QueryType == BitfinexEventType.OrderNew - || bfRequest.QueryType == BitfinexEventType.OrderUpdate - || bfRequest.QueryType == BitfinexEventType.OrderCancel) - { - var orderData = notificationData[4]; - var dataOrderId = orderData[0]?.ToString(); - var dataOrderClientId = orderData[2]?.ToString(); - if (dataOrderId == bfRequest.Id || dataOrderClientId == bfRequest.Id) - { - var desResult = Deserialize(orderData); - if (!desResult) - { - callResult = new CallResult(desResult.Error!); - return true; - } - - callResult = new CallResult(desResult.Data); - return true; - } - } - } - - if (notificationType == BitfinexEventType.OrderCancelMultiRequest) - { - callResult = new CallResult(Deserialize(JToken.Parse("true")).Data); - return true; - } - - if (notificationType == BitfinexEventType.FundingOfferNewRequest - || notificationType == BitfinexEventType.FundingOfferCancelRequest) - { - if (bfRequest.QueryType == BitfinexEventType.FundingOfferCancelRequest) - { - var fundingData = notificationData[4]; - var dataOrderId = fundingData[0]?.ToString(); - if (dataOrderId == bfRequest.Id) - { - var desResult = Deserialize(fundingData); - if (!desResult) - { - callResult = new CallResult(desResult.Error!); - return true; - } - - callResult = new CallResult(desResult.Data); - return true; - } - } - else if(bfRequest.QueryType == BitfinexEventType.FundingOfferNew) - { - var fundingData = notificationData[4]; - var desResult = Deserialize(fundingData); - if (!desResult) - { - callResult = new CallResult(desResult.Error!); - return true; - } - - callResult = new CallResult(desResult.Data); - return true; - } - } - } - - if (bfRequest.QueryType == BitfinexEventType.OrderCancelMulti && eventType == BitfinexEventType.OrderCancel) - { - callResult = new CallResult(Deserialize(JToken.Parse("true")).Data); - return true; - } + parameters.AddOptionalParameter("flags", flags); - return false; + var query = new BitfinexSocketQuery(ExchangeHelpers.NextId().ToString(CultureInfo.InvariantCulture), BitfinexEventType.FundingOfferNew, parameters); + var bitfinexQuery = new BitfinexQuery(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As(result.Data?.Data.Data); } /// - protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken data, out CallResult? callResult) + public async Task> CancelFundingOfferAsync(long id) { - callResult = null; - if (data.Type != JTokenType.Object) - return false; - - var infoEvent = data["event"]?.ToString() == "subscribed"; - var errorEvent = data["event"]?.ToString() == "error"; - if (!infoEvent && !errorEvent) - return false; - - if (infoEvent) - { - var subResponse = Deserialize(data); - if (!subResponse) - { - callResult = new CallResult(subResponse.Error!); - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} subscription failed: " + subResponse.Error); - return false; - } - - var bRequest = (BitfinexSubscriptionRequest)request; - if (!bRequest.CheckResponse(data)) - return false; - - bRequest.ChannelId = subResponse.Data.ChannelId; - callResult = subResponse.As(subResponse.Data); - return true; - } - else + var parameters = new Dictionary { - var subResponse = Deserialize(data); - if (!subResponse) - { - callResult = new CallResult(subResponse.Error!); - _logger.Log(LogLevel.Warning, $"Socket {s.SocketId} subscription failed: " + subResponse.Error); - return false; - } + { "id", id } + }; - var error = new ServerError(subResponse.Data.Code, subResponse.Data.Message); - callResult = new CallResult(error); - _logger.Log(LogLevel.Debug, $"Socket {s.SocketId} subscription failed: " + error); - return true; - } + var query = new BitfinexSocketQuery(id.ToString(CultureInfo.InvariantCulture), BitfinexEventType.FundingOfferCancel, parameters); + var bitfinexQuery = new BitfinexQuery(query); + var result = await QueryAsync(BaseAddress.AppendPath("ws/2"), bitfinexQuery).ConfigureAwait(false); + return result.As(result.Data?.Data.Data); } /// - protected override bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request) + public override string? GetListenerIdentifier(IMessageAccessor message) { - if (message.Type != JTokenType.Array) - return false; - - var array = (JArray)message; - if (array.Count < 2) - return false; + var type = message.GetNodeType(); + if (type == NodeType.Array) + return message.GetValue(_0Path).ToString(); - if (!int.TryParse(array[0].ToString(), out var channelId)) - return false; + var evnt = message.GetValue(_eventPath); + if (evnt == "info") + return "info"; - if (channelId == 0) - return false; - - var subId = ((BitfinexSubscriptionRequest)request).ChannelId; - return channelId == subId && array[1].ToString() != "hb"; + var channel = message.GetValue(_channelPath); + var symbol = message.GetValue(_symbolPath); + var prec = message.GetValue(_precPath); + var freq = message.GetValue(_freqPath); + var len = message.GetValue(_lenPath); + var key = message.GetValue(_keyPath); + var chanId = evnt == "unsubscribed" ? message.GetValue(_chanIdPath) : ""; + return chanId + evnt + channel + symbol + prec + freq + len + key; } - /// - protected override bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier) + private long GenerateClientOrderId() { - if (message.Type == JTokenType.Object) - { - if (identifier == "Info") - return message["event"]?.ToString() == "info"; - if (identifier == "Conf") - return message["event"]?.ToString() == "conf"; - } - - else if (message.Type == JTokenType.Array) - { - var array = (JArray)message; - if (array.Count < 2) - return false; - - if (identifier == "HB") - return array[1].ToString() == "hb"; - - if (!int.TryParse(array[0].ToString(), out var channelId)) - return false; - - if (channelId != 0) - return false; - - var split = identifier.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries); - foreach (var id in split) - { - var events = BitfinexEvents.GetEventsForCategory(id); - var eventTypeString = array[1].ToString(); - var eventType = BitfinexEvents.EventMapping[eventTypeString]; - var evnt = events.SingleOrDefault(e => e.EventType == eventType); - if (evnt != null) - return true; - } - } - - return false; + var buffer = new byte[8]; + _random.NextBytes(buffer); + return (long)Math.Round(Math.Abs(BitConverter.ToInt32(buffer, 0)) / 1000m); } } } diff --git a/Bitfinex.Net/Converters/OrderBookEntryConverter.cs b/Bitfinex.Net/Converters/OrderBookEntryConverter.cs index 73bb7df..8ebb6e6 100644 --- a/Bitfinex.Net/Converters/OrderBookEntryConverter.cs +++ b/Bitfinex.Net/Converters/OrderBookEntryConverter.cs @@ -72,7 +72,12 @@ private static BitfinexOrderBookEntry ParseEntry(JArray data) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - throw new NotImplementedException(); + var obj = (BitfinexOrderBookEntry)value!; + writer.WriteStartArray(); + writer.WriteValue(obj.RawPrice); + writer.WriteValue(obj.Count); + writer.WriteValue(obj.RawQuantity); + writer.WriteEndArray(); } } } diff --git a/Bitfinex.Net/Enums/BitfinexEventType.cs b/Bitfinex.Net/Enums/BitfinexEventType.cs index 5f0a4e3..bbcae84 100644 --- a/Bitfinex.Net/Enums/BitfinexEventType.cs +++ b/Bitfinex.Net/Enums/BitfinexEventType.cs @@ -1,4 +1,6 @@ -namespace Bitfinex.Net.Enums +using CryptoExchange.Net.Attributes; + +namespace Bitfinex.Net.Enums { /// /// Socket event types @@ -8,199 +10,209 @@ public enum BitfinexEventType /// /// Heartbeat /// + [Map("hb")] HeartBeat, /// /// Balance update /// + [Map("bu")] BalanceUpdate, /// /// Position snapshot /// + [Map("ps")] PositionSnapshot, /// /// New position /// + [Map("pn")] PositionNew, /// /// Position update /// + [Map("pu")] PositionUpdate, /// /// Position closed /// + [Map("pc")] PositionClose, /// /// Wallet snapshot /// + [Map("ws")] WalletSnapshot, /// /// Wallet update /// + [Map("wu")] WalletUpdate, /// /// Orders snapshot /// + [Map("os")] OrderSnapshot, /// /// New order /// + [Map("on")] OrderNew, /// /// New order request /// + [Map("on-req")] OrderNewRequest, /// /// Order update /// + [Map("ou")] OrderUpdate, /// /// Order update request /// + [Map("ou-req")] OrderUpdateRequest, /// /// Order canceled /// + [Map("oc")] OrderCancel, /// /// Order cancel request /// + [Map("oc-req")] OrderCancelRequest, /// /// Multiple orders canceled /// + [Map("oc_multi")] OrderCancelMulti, /// /// Multiple orders cancel request /// + [Map("oc_multi-req")] OrderCancelMultiRequest, - /// - /// Trade snapshot - /// - TradeSnapshot, /// /// Trade executed /// + [Map("te")] TradeExecuted, /// /// Trade execution update /// + [Map("tu")] TradeExecutionUpdate, /// /// Funding trade execution /// + [Map("fte")] FundingTradeExecution, /// /// Funding trade update /// + [Map("ftu")] FundingTradeUpdate, - /// - /// Historical order snapshot - /// - HistoricalOrderSnapshot, - /// /// Margin info snapshot /// + [Map("mis")] MarginInfoSnapshot, /// /// Margin info update /// + [Map("miu")] MarginInfoUpdate, /// /// Notification /// + [Map("n")] Notification, /// /// Funding offer snapshot /// + [Map("fos")] FundingOfferSnapshot, /// /// New funding offer /// + [Map("fon")] FundingOfferNew, /// /// New funding offer request /// + [Map("fon-req")] FundingOfferNewRequest, /// /// Funding offer update /// + [Map("fou")] FundingOfferUpdate, /// /// Funding offer canceled /// + [Map("foc")] FundingOfferCancel, /// /// Funding offer cancel request /// + [Map("foc-req")] FundingOfferCancelRequest, - /// - /// Historical funding offer snapshot - /// - HistoricalFundingOfferSnapshot, - /// /// Funding credits snapshot /// + [Map("fcs")] FundingCreditsSnapshot, /// /// New funding credits /// + [Map("fcn")] FundingCreditsNew, /// /// Funding credits update /// + [Map("fcu")] FundingCreditsUpdate, /// /// Funding credits closed /// + [Map("fcc")] FundingCreditsClose, - - /// - /// Historical funding credits snapshot - /// - HistoricalFundingCreditsSnapshot, - + /// /// Funding loan snapshot /// + [Map("fls")] FundingLoanSnapshot, /// /// New funding loan /// + [Map("fln")] FundingLoanNew, /// /// Funding loan update /// + [Map("flu")] FundingLoanUpdate, /// /// Funding loan closed /// + [Map("flc")] FundingLoanClose, - /// - /// Historical funding loan snapshot - /// - HistoricalFundingLoanSnapshot, - - /// - /// Historical funding trade snapshot - /// - HistoricalFundingTradeSnapshot, - /// /// Custom user price alert /// + [Map("uac")] UserCustomPriceAlert } } diff --git a/Bitfinex.Net/ExtensionMethods/BitfinexExtensionMethods.cs b/Bitfinex.Net/ExtensionMethods/BitfinexExtensionMethods.cs new file mode 100644 index 0000000..ade1f7e --- /dev/null +++ b/Bitfinex.Net/ExtensionMethods/BitfinexExtensionMethods.cs @@ -0,0 +1,51 @@ +using System; +using System.Text.RegularExpressions; + +namespace Bitfinex.Net.ExtensionMethods +{ + /// + /// Extension methods specific to using the Bitfinex API + /// + public static class BitfinexExtensionMethods + { + /// + /// Validate the string is a valid Bitfinex symbol. + /// + /// string to validate + public static void ValidateBitfinexSymbol(this string symbolString) + { + if (string.IsNullOrEmpty(symbolString)) + throw new ArgumentException("Symbol is not provided"); + + if (!Regex.IsMatch(symbolString, "^([t]([A-Z0-9|:]{6,}))$") && !Regex.IsMatch(symbolString, "^([f]([A-Z0-9]{3,}))$")) + throw new ArgumentException($"{symbolString} is not a valid Bitfinex symbol. Should be [t][QuoteAsset][BaseAsset] for trading pairs " + + "or [f][Asset] for margin symbols, e.g. tBTCUSD or fUSD"); + } + + /// + /// Validate the string is a valid Bitfinex symbol. + /// + /// string to validate + public static void ValidateBitfinexFundingSymbol(this string symbolString) + { + if (string.IsNullOrEmpty(symbolString)) + throw new ArgumentException("Symbol is not provided"); + + if (!Regex.IsMatch(symbolString, "^([f]([A-Z0-9]{3,}))$")) + throw new ArgumentException($"{symbolString} is not a valid Bitfinex funding symbol. Should be [f][Asset] for funding symbols, e.g. fUSD"); + } + + /// + /// Validate the string is a valid Bitfinex symbol. + /// + /// string to validate + public static void ValidateBitfinexTradingSymbol(this string symbolString) + { + if (string.IsNullOrEmpty(symbolString)) + throw new ArgumentException("Symbol is not provided"); + + if (!Regex.IsMatch(symbolString, "^([t]([A-Z0-9|:]{6,}))$")) + throw new ArgumentException($"{symbolString} is not a valid Bitfinex symbol. Should be [t][QuoteAsset][BaseAsset] for trading pairs, e.g. tBTCUSD"); + } + } +} diff --git a/Bitfinex.Net/ExtensionMethods/CryptoClientExtensions.cs b/Bitfinex.Net/ExtensionMethods/CryptoClientExtensions.cs new file mode 100644 index 0000000..00c671c --- /dev/null +++ b/Bitfinex.Net/ExtensionMethods/CryptoClientExtensions.cs @@ -0,0 +1,25 @@ +using Bitfinex.Net.Clients; +using Bitfinex.Net.Interfaces.Clients; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Extensions for the ICryptoRestClient and ICryptoSocketClient interfaces + /// + public static class CryptoClientExtensions + { + /// + /// Get the Bitfinex REST Api client + /// + /// + /// + public static IBitfinexRestClient Bitfinex(this ICryptoRestClient baseClient) => baseClient.TryGet(() => new BitfinexRestClient()); + + /// + /// Get the Bitfinex Websocket Api client + /// + /// + /// + public static IBitfinexSocketClient Bitfinex(this ICryptoSocketClient baseClient) => baseClient.TryGet(() => new BitfinexSocketClient()); + } +} diff --git a/Bitfinex.Net/BitfinexHelpers.cs b/Bitfinex.Net/ExtensionMethods/ServiceCollectionExtensions.cs similarity index 56% rename from Bitfinex.Net/BitfinexHelpers.cs rename to Bitfinex.Net/ExtensionMethods/ServiceCollectionExtensions.cs index 90d4009..cb6ab09 100644 --- a/Bitfinex.Net/BitfinexHelpers.cs +++ b/Bitfinex.Net/ExtensionMethods/ServiceCollectionExtensions.cs @@ -1,22 +1,18 @@ using Bitfinex.Net.Clients; +using Bitfinex.Net.Interfaces; using Bitfinex.Net.Interfaces.Clients; using Bitfinex.Net.Objects.Options; -using Microsoft.Extensions.DependencyInjection; +using Bitfinex.Net.SymbolOrderBooks; using System; -using System.Net.Http; using System.Net; -using System.Text.RegularExpressions; -using Bitfinex.Net.Interfaces; -using Bitfinex.Net.SymbolOrderBooks; -using CryptoExchange.Net.Interfaces.CommonClients; -using Bitfinex.Net.Clients.SpotApi; +using System.Net.Http; -namespace Bitfinex.Net +namespace Microsoft.Extensions.DependencyInjection { /// - /// Helper functions + /// Extensions for DI /// - public static class BitfinexHelpers + public static class ServiceCollectionExtensions { /// /// Add the IBitfinexClient and IBitfinexSocketClient to the sevice collection so they can be injected @@ -68,45 +64,5 @@ public static IServiceCollection AddBitfinex( services.Add(new ServiceDescriptor(typeof(IBitfinexSocketClient), typeof(BitfinexSocketClient), socketClientLifeTime.Value)); return services; } - - /// - /// Validate the string is a valid Bitfinex symbol. - /// - /// string to validate - public static void ValidateBitfinexSymbol(this string symbolString) - { - if (string.IsNullOrEmpty(symbolString)) - throw new ArgumentException("Symbol is not provided"); - - if (!Regex.IsMatch(symbolString, "^([t]([A-Z0-9|:]{6,}))$") && !Regex.IsMatch(symbolString, "^([f]([A-Z0-9]{3,}))$")) - throw new ArgumentException($"{symbolString} is not a valid Bitfinex symbol. Should be [t][QuoteAsset][BaseAsset] for trading pairs " + - "or [f][Asset] for margin symbols, e.g. tBTCUSD or fUSD"); - } - - /// - /// Validate the string is a valid Bitfinex symbol. - /// - /// string to validate - public static void ValidateBitfinexFundingSymbol(this string symbolString) - { - if (string.IsNullOrEmpty(symbolString)) - throw new ArgumentException("Symbol is not provided"); - - if (!Regex.IsMatch(symbolString, "^([f]([A-Z0-9]{3,}))$")) - throw new ArgumentException($"{symbolString} is not a valid Bitfinex funding symbol. Should be [f][Asset] for funding symbols, e.g. fUSD"); - } - - /// - /// Validate the string is a valid Bitfinex symbol. - /// - /// string to validate - public static void ValidateBitfinexTradingSymbol(this string symbolString) - { - if (string.IsNullOrEmpty(symbolString)) - throw new ArgumentException("Symbol is not provided"); - - if (!Regex.IsMatch(symbolString, "^([t]([A-Z0-9|:]{6,}))$")) - throw new ArgumentException($"{symbolString} is not a valid Bitfinex symbol. Should be [t][QuoteAsset][BaseAsset] for trading pairs, e.g. tBTCUSD"); - } } } diff --git a/Bitfinex.Net/Interfaces/Clients/SpotApi/IBitfinexSocketClientSpotApi.cs b/Bitfinex.Net/Interfaces/Clients/SpotApi/IBitfinexSocketClientSpotApi.cs index ec0998d..44a3434 100644 --- a/Bitfinex.Net/Interfaces/Clients/SpotApi/IBitfinexSocketClientSpotApi.cs +++ b/Bitfinex.Net/Interfaces/Clients/SpotApi/IBitfinexSocketClientSpotApi.cs @@ -7,7 +7,7 @@ using Bitfinex.Net.Objects.Models.Socket; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -using CryptoExchange.Net.Sockets; +using CryptoExchange.Net.Objects.Sockets; namespace Bitfinex.Net.Interfaces.Clients.SpotApi { @@ -135,39 +135,31 @@ public interface IBitfinexSocketClientSpotApi : ISocketApiClient, IDisposable /// Data handler for order updates. Can be null if not interested /// Data handler for trade execution updates. Can be null if not interested /// Data handler for position updates. Can be null if not interested + /// Data handler for funding offer updates. Can be null if not interested + /// Data handler for funding credit updates. Can be null if not interested + /// Data handler for funding loan updates. Can be null if not interested + /// Data handler for wallet updates. Can be null if not interested + /// Data handler for balance updates. Can be null if not interested + /// Data handler for funding trade updates. Can be null if not interested + /// Data handler for funding info updates. Can be null if not interested + /// Data handler for margin base updates. Can be null if not interested + /// Data handler for margin symbol updates. Can be null if not interested /// Cancellation token for closing this subscription /// - Task> SubscribeToUserTradeUpdatesAsync( - Action>>> orderHandler, - Action>>> tradeHandler, - Action>>> positionHandler, - CancellationToken ct = default); - - /// - /// Subscribe to wallet information updates - /// - /// - /// Data handler for wallet updates - /// Cancellation token for closing this subscription - /// - Task> SubscribeToBalanceUpdatesAsync(Action>>> walletHandler, CancellationToken ct = default); - - /// - /// Subscribe to funding information updates - /// - /// - /// - /// - /// Subscribe to funding offer updates. Can be null if not interested - /// Subscribe to funding credit updates. Can be null if not interested - /// Subscribe to funding loan updates. Can be null if not interested - /// Cancellation token for closing this subscription - /// - Task> SubscribeToFundingUpdatesAsync( - Action>>> fundingOfferHandler, - Action>>> fundingCreditHandler, - Action>>> fundingLoanHandler, - CancellationToken ct = default); + Task> SubscribeToUserUpdatesAsync( + Action>>? orderHandler = null, + Action>>? positionHandler = null, + Action>>? fundingOfferHandler = null, + Action>>? fundingCreditHandler = null, + Action>>? fundingLoanHandler = null, + Action>>? walletHandler = null, + Action>? balanceHandler = null, + Action>? tradeHandler = null, + Action>? fundingTradeHandler = null, + Action>? fundingInfoHandler = null, + Action>? marginBaseHandler = null, + Action>? marginSymbolHandler = null, + CancellationToken ct = default); /// /// Places a new order @@ -190,6 +182,13 @@ Task> SubscribeToFundingUpdatesAsync( /// Task> PlaceOrderAsync(OrderSide side, OrderType type, string symbol, decimal quantity, long? groupId = null, long? clientOrderId = null, decimal? price = null, decimal? priceTrailing = null, decimal? priceAuxiliaryLimit = null, decimal? priceOcoStop = null, OrderFlags? flags = null, int? leverage = null, DateTime? cancelTime = null, string? affiliateCode = null); + /// + /// Cancel all orders + /// + /// + /// + Task>> CancelAllOrdersAsync(); + /// /// Updates an order /// @@ -218,7 +217,7 @@ Task> SubscribeToFundingUpdatesAsync( /// /// The group id to cancel /// True if successfully committed on server - Task> CancelOrdersByGroupIdAsync(long groupOrderId); + Task>> CancelOrdersByGroupIdAsync(long groupOrderId); /// /// Cancels multiple orders based on their groupIds @@ -226,7 +225,7 @@ Task> SubscribeToFundingUpdatesAsync( /// /// The group ids to cancel /// True if successfully committed on server - Task> CancelOrdersByGroupIdsAsync(IEnumerable groupOrderIds); + Task>> CancelOrdersByGroupIdsAsync(IEnumerable groupOrderIds); /// /// Cancels multiple orders based on their order ids @@ -234,7 +233,7 @@ Task> SubscribeToFundingUpdatesAsync( /// /// The order ids to cancel /// True if successfully committed on server - Task> CancelOrdersAsync(IEnumerable orderIds); + Task>> CancelOrdersAsync(IEnumerable orderIds); /// /// Cancels multiple orders based on their clientOrderIds @@ -242,7 +241,7 @@ Task> SubscribeToFundingUpdatesAsync( /// /// The client order ids to cancel, listed as (clientOrderId, Day) pair. ClientOrderIds are unique per day, so timestamp should be provided /// True if successfully committed on server - Task> CancelOrdersByClientOrderIdsAsync(Dictionary clientOrderIds); + Task>> CancelOrdersByClientOrderIdsAsync(Dictionary clientOrderIds); /// /// Submit a new funding offer diff --git a/Bitfinex.Net/Objects/Internal/BitfinexAuthenticationResponse.cs b/Bitfinex.Net/Objects/Internal/BitfinexAuthenticationResponse.cs deleted file mode 100644 index 11dcef5..0000000 --- a/Bitfinex.Net/Objects/Internal/BitfinexAuthenticationResponse.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; - -namespace Bitfinex.Net.Objects.Internal -{ - internal class BitfinexAuthenticationResponse - { - [JsonProperty("event")] - public string Event { get; set; } = string.Empty; - [JsonProperty("status")] - public string Status { get; set; } = string.Empty; - [JsonProperty("userId")] - public long UserId { get; set; } - [JsonProperty("chanId")] - public long ChannelId { get; set; } - [JsonProperty("auth_id")] - public string AuthenticationId { get; set; } = string.Empty; - [JsonProperty("caps")] - public BitfinexApiKeyPermissions Permissions { get; set; } = default!; - - // In case of error - [JsonProperty("code")] - public int ErrorCode { get; set; } - [JsonProperty("msg")] - public string? ErrorMessage { get; set; } - } - - internal class BitfinexApiKeyPermissions - { - public BitfinexReadWritePermission Account { get; set; } = default!; - public BitfinexReadWritePermission History { get; set; } = default!; - public BitfinexReadWritePermission Orders { get; set; } = default!; - public BitfinexReadWritePermission Positions { get; set; } = default!; - public BitfinexReadWritePermission Funding { get; set; } = default!; - public BitfinexReadWritePermission Wallets { get; set; } = default!; - public BitfinexReadWritePermission Withdraw { get; set; } = default!; - } - - internal class BitfinexReadWritePermission - { - public bool Read { get; set; } - public bool Write { get; set; } - } -} diff --git a/Bitfinex.Net/Objects/Internal/BitfinexChecksum.cs b/Bitfinex.Net/Objects/Internal/BitfinexChecksum.cs new file mode 100644 index 0000000..4155812 --- /dev/null +++ b/Bitfinex.Net/Objects/Internal/BitfinexChecksum.cs @@ -0,0 +1,16 @@ +using CryptoExchange.Net.Converters; +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Internal +{ + [JsonConverter(typeof(ArrayConverter))] + internal class BitfinexChecksum + { + [ArrayProperty(0)] + public int ChannelId { get; set; } + [ArrayProperty(1)] + public string Topic { get; set; } = string.Empty; + [ArrayProperty(2)] + public int Checksum { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Internal/BitfinexEvents.cs b/Bitfinex.Net/Objects/Internal/BitfinexEvents.cs index c6a8caa..5b1ef61 100644 --- a/Bitfinex.Net/Objects/Internal/BitfinexEvents.cs +++ b/Bitfinex.Net/Objects/Internal/BitfinexEvents.cs @@ -89,7 +89,6 @@ public static IEnumerable GetEventsForCategory(string cat) { "tu", BitfinexEventType.TradeExecutionUpdate }, { "fte", BitfinexEventType.FundingTradeExecution }, { "ftu", BitfinexEventType.FundingTradeUpdate }, - { "hos", BitfinexEventType.HistoricalOrderSnapshot }, { "mis", BitfinexEventType.MarginInfoSnapshot }, { "miu", BitfinexEventType.MarginInfoUpdate }, { "n", BitfinexEventType.Notification }, @@ -99,18 +98,14 @@ public static IEnumerable GetEventsForCategory(string cat) { "fou", BitfinexEventType.FundingOfferUpdate }, { "foc", BitfinexEventType.FundingOfferCancel }, { "foc-req", BitfinexEventType.FundingOfferCancelRequest }, - { "hfos", BitfinexEventType.HistoricalFundingOfferSnapshot }, { "fcs", BitfinexEventType.FundingCreditsSnapshot }, { "fcn", BitfinexEventType.FundingCreditsNew }, { "fcu", BitfinexEventType.FundingCreditsUpdate }, { "fcc", BitfinexEventType.FundingCreditsClose }, - { "hfcs", BitfinexEventType.HistoricalFundingCreditsSnapshot }, { "fls", BitfinexEventType.FundingLoanSnapshot }, { "fln", BitfinexEventType.FundingLoanNew }, { "flu", BitfinexEventType.FundingLoanUpdate }, { "flc", BitfinexEventType.FundingLoanClose }, - { "hfls", BitfinexEventType.HistoricalFundingLoanSnapshot }, - { "hfts", BitfinexEventType.HistoricalFundingTradeSnapshot }, { "uac", BitfinexEventType.UserCustomPriceAlert } }; } diff --git a/Bitfinex.Net/Objects/Internal/BitfinexResponse.cs b/Bitfinex.Net/Objects/Internal/BitfinexResponse.cs deleted file mode 100644 index 9765613..0000000 --- a/Bitfinex.Net/Objects/Internal/BitfinexResponse.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json; - -namespace Bitfinex.Net.Objects.Internal -{ - internal class BitfinexResponse - { - public string Event { get; set; } = string.Empty; - public string Channel { get; set; } = string.Empty; - } - - internal class BitfinexSubscribeResponse: BitfinexResponse - { - [JsonProperty("chanId")] - public int ChannelId { get; set; } - } - - internal class BitfinexErrorResponse: BitfinexResponse - { - [JsonProperty("msg")] - public string Message { get; set; } = string.Empty; - [JsonProperty("code")] - public int Code { get; set; } - } -} diff --git a/Bitfinex.Net/Objects/Internal/BitfinexSocketConfig.cs b/Bitfinex.Net/Objects/Internal/BitfinexSocketConfig.cs index 46c664c..fed9d84 100644 --- a/Bitfinex.Net/Objects/Internal/BitfinexSocketConfig.cs +++ b/Bitfinex.Net/Objects/Internal/BitfinexSocketConfig.cs @@ -4,7 +4,8 @@ namespace Bitfinex.Net.Objects.Internal { internal class BitfinexSocketConfig { - [JsonProperty("event")] public string Event { get; set; } = string.Empty; + [JsonProperty("event")] + public string Event { get; set; } = string.Empty; [JsonProperty("flags")] public int Flags { get; set; } diff --git a/Bitfinex.Net/Objects/Models/BitfinexBalance.cs b/Bitfinex.Net/Objects/Models/BitfinexBalance.cs new file mode 100644 index 0000000..0f69f1e --- /dev/null +++ b/Bitfinex.Net/Objects/Models/BitfinexBalance.cs @@ -0,0 +1,24 @@ +using CryptoExchange.Net.Converters; +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Models +{ + /// + /// Balance + /// + [JsonConverter(typeof(ArrayConverter))] + public class BitfinexBalance + { + /// + /// Total Assets Under Management + /// + [ArrayProperty(0)] + public decimal TotalAssets { get; set; } + + /// + /// Net Assets Under Management (total assets - total liabilities) + /// + [ArrayProperty(1)] + public decimal NetAssets { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Models/BitfinexFundingAutoRenew.cs b/Bitfinex.Net/Objects/Models/BitfinexFundingAutoRenew.cs index 921d83e..ddac2d6 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexFundingAutoRenew.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexFundingAutoRenew.cs @@ -1,6 +1,5 @@ using CryptoExchange.Net.Converters; using Newtonsoft.Json; -using System; namespace Bitfinex.Net.Objects.Models { diff --git a/Bitfinex.Net/Objects/Models/BitfinexFundingOrderBook.cs b/Bitfinex.Net/Objects/Models/BitfinexFundingOrderBook.cs index c6ac12c..4bbb921 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexFundingOrderBook.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexFundingOrderBook.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using CryptoExchange.Net.Interfaces; namespace Bitfinex.Net.Objects.Models { diff --git a/Bitfinex.Net/Objects/Models/BitfinexKeyValue.cs b/Bitfinex.Net/Objects/Models/BitfinexKeyValue.cs index 87468cc..1543e72 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexKeyValue.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexKeyValue.cs @@ -1,9 +1,6 @@ using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Converters; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace Bitfinex.Net.Objects.Models { diff --git a/Bitfinex.Net/Objects/Models/BitfinexOrderBook.cs b/Bitfinex.Net/Objects/Models/BitfinexOrderBook.cs index 524241c..44faefe 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexOrderBook.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexOrderBook.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using CryptoExchange.Net.Interfaces; namespace Bitfinex.Net.Objects.Models { diff --git a/Bitfinex.Net/Objects/Models/BitfinexOrderBookEntry.cs b/Bitfinex.Net/Objects/Models/BitfinexOrderBookEntry.cs index db0352a..e3ef055 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexOrderBookEntry.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexOrderBookEntry.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Converters; +using Bitfinex.Net.Converters; +using CryptoExchange.Net.Converters; using CryptoExchange.Net.Interfaces; using Newtonsoft.Json; @@ -13,6 +14,7 @@ public class BitfinexOrderBookBase { } /// /// Order book entry /// + [JsonConverter(typeof(OrderBookEntryConverter))] public class BitfinexOrderBookEntry: BitfinexOrderBookBase, ISymbolOrderBookEntry { /// diff --git a/Bitfinex.Net/Objects/Models/BitfinexRawFundingOrderBook.cs b/Bitfinex.Net/Objects/Models/BitfinexRawFundingOrderBook.cs index 07c0bb8..a8afa30 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexRawFundingOrderBook.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexRawFundingOrderBook.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using CryptoExchange.Net.Interfaces; namespace Bitfinex.Net.Objects.Models { diff --git a/Bitfinex.Net/Objects/Models/BitfinexRawOrderBook.cs b/Bitfinex.Net/Objects/Models/BitfinexRawOrderBook.cs index 78774cb..b6c58ef 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexRawOrderBook.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexRawOrderBook.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using CryptoExchange.Net.Interfaces; namespace Bitfinex.Net.Objects.Models { diff --git a/Bitfinex.Net/Objects/Models/BitfinexSummary.cs b/Bitfinex.Net/Objects/Models/BitfinexSummary.cs index 6bf1c6b..e6e2458 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexSummary.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexSummary.cs @@ -1,5 +1,4 @@ -using Bitfinex.Net.Objects.Models.V1; -using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Converters; using Newtonsoft.Json; using System.Collections.Generic; diff --git a/Bitfinex.Net/Objects/Models/BitfinexTradeSimple.cs b/Bitfinex.Net/Objects/Models/BitfinexTradeSimple.cs index 5b2e851..2ae108e 100644 --- a/Bitfinex.Net/Objects/Models/BitfinexTradeSimple.cs +++ b/Bitfinex.Net/Objects/Models/BitfinexTradeSimple.cs @@ -1,5 +1,4 @@ using System; -using Bitfinex.Net.Enums; using CryptoExchange.Net.Converters; using Newtonsoft.Json; @@ -36,11 +35,5 @@ public class BitfinexTradeSimple /// [ArrayProperty(4)] public int? Period { get; set; } - - /// - /// The type of update - /// - [JsonIgnore] - public BitfinexEventType UpdateType { get; set; } = BitfinexEventType.TradeSnapshot; } } diff --git a/Bitfinex.Net/Objects/Models/Socket/BitfinexNotification.cs b/Bitfinex.Net/Objects/Models/Socket/BitfinexNotification.cs new file mode 100644 index 0000000..1e7da36 --- /dev/null +++ b/Bitfinex.Net/Objects/Models/Socket/BitfinexNotification.cs @@ -0,0 +1,24 @@ +using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Converters; +using Newtonsoft.Json; +using System; + +namespace Bitfinex.Net.Objects.Models.Socket +{ + [JsonConverter(typeof(ArrayConverter))] + internal class BitfinexNotification + { + [ArrayProperty(0)] + [JsonConverter(typeof(DateTimeConverter))] + public DateTime Timestamp { get; set; } + [ArrayProperty(1)] + public string Event { get; set; } = string.Empty; + [ArrayProperty(4)] + [JsonConversion] + public T Data { get; set; } = default!; + [ArrayProperty(6)] + public string Result { get; set; } = string.Empty; + [ArrayProperty(7)] + public string? ErrorMessage { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketEvent.cs b/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketEvent.cs index 8d73090..d082908 100644 --- a/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketEvent.cs +++ b/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketEvent.cs @@ -1,4 +1,5 @@ using Bitfinex.Net.Enums; +using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Converters; using Newtonsoft.Json; @@ -19,12 +20,15 @@ public class BitfinexSocketEvent /// /// The type of the event /// + [ArrayProperty(1)] + [JsonConverter(typeof(EnumConverter))] public BitfinexEventType EventType { get; set; } /// /// The data /// - [ArrayProperty(2), JsonConverter(typeof(ArrayConverter))] + [ArrayProperty(2)] + [JsonConversion] public T Data { get; set; } = default!; /// diff --git a/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketInfo.cs b/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketInfo.cs new file mode 100644 index 0000000..e193183 --- /dev/null +++ b/Bitfinex.Net/Objects/Models/Socket/BitfinexSocketInfo.cs @@ -0,0 +1,29 @@ +using Bitfinex.Net.Converters; +using Bitfinex.Net.Enums; +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Models.Socket +{ + internal class BitfinexSocketInfo + { + [JsonProperty("event")] + public string Event { get; set; } = string.Empty; + [JsonProperty("version")] + public int? Version { get; set; } + [JsonProperty("serverId")] + public string? ServerId { get; set; } = string.Empty; + [JsonProperty("platform")] + public BitfinexSocketInfoDetails? Platform { get; set; } = null!; + [JsonProperty("code")] + public int? Code { get; set; } + [JsonProperty("msg")] + public string? Message { get; set; } + } + + internal class BitfinexSocketInfoDetails + { + [JsonProperty("status")] + [JsonConverter(typeof(PlatformStatusConverter))] + public PlatformStatus Status { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/BitfinexBookRequest.cs b/Bitfinex.Net/Objects/Sockets/BitfinexBookRequest.cs new file mode 100644 index 0000000..6c8c8ee --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/BitfinexBookRequest.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Sockets +{ + internal class BitfinexBookRequest: BitfinexRequest + { + [JsonProperty("prec")] + public string? Precision { get; set; } + [JsonProperty("freq")] + public string? Frequency { get; set; } + [JsonProperty("len")] + public string? Length { get; set; } + [JsonProperty("key")] + public string? Key { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/BitfinexConfQuery.cs b/Bitfinex.Net/Objects/Sockets/BitfinexConfQuery.cs new file mode 100644 index 0000000..f0ebf8b --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/BitfinexConfQuery.cs @@ -0,0 +1,16 @@ +using Bitfinex.Net.Objects.Internal; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; + +namespace Bitfinex.Net.Objects.Sockets +{ + internal class BitfinexConfQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } = new HashSet { "conf" }; + + public BitfinexConfQuery(int flags) : base(new BitfinexSocketConfig { Event = "conf", Flags = flags }, false, 1) + { + } + + } +} diff --git a/Bitfinex.Net/Objects/Sockets/BitfinexRequest.cs b/Bitfinex.Net/Objects/Sockets/BitfinexRequest.cs new file mode 100644 index 0000000..ab422e4 --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/BitfinexRequest.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Sockets +{ + internal class BitfinexRequest + { + [JsonProperty("event")] + public string Event { get; set; } = string.Empty; + [JsonProperty("channel")] + public string Channel { get; set; } = string.Empty; + [JsonProperty("symbol")] + public string? Symbol { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/BitfinexResponse.cs b/Bitfinex.Net/Objects/Sockets/BitfinexResponse.cs new file mode 100644 index 0000000..c5fc2db --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/BitfinexResponse.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Sockets +{ + internal class BitfinexResponse + { + [JsonProperty("code")] + public int? Code { get; set; } + [JsonProperty("event")] + public string Event { get; set; } = string.Empty; + [JsonProperty("msg")] + public string Message { get; set; } = string.Empty; + [JsonProperty("status")] + public string Status { get; set; } = string.Empty; + [JsonProperty("channel")] + public string Channel { get; set; } = string.Empty; + [JsonProperty("symbol")] + public string? Symbol { get; set; } + [JsonProperty("pair")] + public string? Pair { get; set; } + [JsonProperty("chanId")] + public int? ChannelId { get; set; } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/BitfinexUpdate.cs b/Bitfinex.Net/Objects/Sockets/BitfinexUpdate.cs new file mode 100644 index 0000000..4dac9c7 --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/BitfinexUpdate.cs @@ -0,0 +1,28 @@ +using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Converters; +using Newtonsoft.Json; + +namespace Bitfinex.Net.Objects.Sockets +{ + [JsonConverter(typeof(ArrayConverter))] + internal class BitfinexUpdate + { + [ArrayProperty(0)] + public int ChannelId { get; set; } + [ArrayProperty(1)] + [JsonConversion] + public T Data { get; set; } = default!; + } + + [JsonConverter(typeof(ArrayConverter))] + internal class BitfinexTopicUpdate + { + [ArrayProperty(0)] + public int ChannelId { get; set; } + [ArrayProperty(1)] + public string Topic { get; set; } = string.Empty; + [ArrayProperty(2)] + [JsonConversion] + public T Data { get; set; } = default!; + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Queries/BitfinexAuthQuery.cs b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexAuthQuery.cs new file mode 100644 index 0000000..65722c3 --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexAuthQuery.cs @@ -0,0 +1,26 @@ +using Bitfinex.Net.Objects.Internal; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bitfinex.Net.Objects.Sockets.Queries +{ + internal class BitfinexAuthQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } = new HashSet { "auth" }; + + public BitfinexAuthQuery(BitfinexAuthentication authRequest) : base(authRequest, true) + { + } + + public override Task> HandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data.Status != "OK") + return Task.FromResult(new CallResult(new ServerError(message.Data.Code!.Value, message.Data.Message!))); + + return Task.FromResult(new CallResult(message.Data, message.OriginalData, null)); + } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Queries/BitfinexQuery.cs b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexQuery.cs new file mode 100644 index 0000000..4845f72 --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexQuery.cs @@ -0,0 +1,39 @@ +using Bitfinex.Net.Objects.Internal; +using Bitfinex.Net.Objects.Models.Socket; +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 System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bitfinex.Net.Objects.Sockets.Queries +{ + internal class BitfinexQuery : Query>> + { + private static readonly MessagePath _1Path = MessagePath.Get().Index(1); + public override HashSet ListenerIdentifiers { get; set; } = new HashSet { "0" }; + + public BitfinexQuery(BitfinexSocketQuery request) : base(request, true, 1) + { + } + + public override Type? GetMessageType(IMessageAccessor message) + { + if (message.GetValue(_1Path) != "n") + return null; + + return typeof(BitfinexSocketEvent>); + } + + public override Task>>> HandleMessageAsync(SocketConnection connection, DataEvent>> message) + { + if (message.Data.Data.Result != "SUCCESS") + return Task.FromResult(new CallResult>>(new ServerError(message.Data.Data.ErrorMessage!))); + + return Task.FromResult(new CallResult>>(message.Data)); + } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Queries/BitfinexSubQuery.cs b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexSubQuery.cs new file mode 100644 index 0000000..4e27a17 --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexSubQuery.cs @@ -0,0 +1,42 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bitfinex.Net.Objects.Sockets.Queries +{ + internal class BitfinexSubQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } + + public BitfinexSubQuery(string evnt, string channel, string? symbol, string? precision, string? frequency, string? length, string? key) : base(new BitfinexBookRequest + { + Channel = channel, + Symbol = symbol, + Event = evnt, + Frequency = frequency, + Length = length, + Precision = precision, + Key = key + }, false, 1) + { + if (evnt == "subscribe" || evnt == "unsubscribe") + evnt += "d"; + + ListenerIdentifiers = new HashSet + { + evnt + channel + symbol + precision + frequency + length + key, + "error" + channel + symbol + precision + frequency + length + key + }; + } + + public override Task> HandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data.Event == "error") + return Task.FromResult(new CallResult(new ServerError(message.Data.Message!))); + + return Task.FromResult(new CallResult(message.Data)); + } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Queries/BitfinexUnsubQuery.cs b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexUnsubQuery.cs new file mode 100644 index 0000000..9ef651e --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Queries/BitfinexUnsubQuery.cs @@ -0,0 +1,19 @@ +using Bitfinex.Net.Objects.Internal; +using CryptoExchange.Net.Sockets; +using System.Collections.Generic; + +namespace Bitfinex.Net.Objects.Sockets.Queries +{ + internal class BitfinexUnsubQuery : Query + { + public override HashSet ListenerIdentifiers { get; set; } + + public BitfinexUnsubQuery(int channelId) : base(new BitfinexUnsubscribeRequest(channelId), false, 1) + { + ListenerIdentifiers = new HashSet + { + channelId.ToString() + "unsubscribed" + }; + } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexInfoSubscription.cs b/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexInfoSubscription.cs new file mode 100644 index 0000000..0047b7b --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexInfoSubscription.cs @@ -0,0 +1,52 @@ +using Bitfinex.Net.Objects.Models.Socket; +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 Bitfinex.Net.Objects.Sockets.Subscriptions +{ + internal class BitfinexInfoSubscription : SystemSubscription + { + public override HashSet ListenerIdentifiers { get; set; } = new HashSet { "info" }; + + public BitfinexInfoSubscription(ILogger logger) : base(logger, false) + { + } + + public override Task HandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data.Code == null) + { + // welcome event, send a config message for receiving checsum updates for order book subscriptions + _ = connection.SendAndWaitQueryAsync(new BitfinexConfQuery(131072)); + return Task.FromResult(new CallResult(null)); + } + + var code = message.Data.Code; + switch (code) + { + case 20051: + _logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] code {code} received, reconnecting socket"); + connection.PausedActivity = true; // Prevent new operations to be send + _ = connection.TriggerReconnectAsync(); + break; + case 20060: + _logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] code {code} received, entering maintenance mode"); + connection.PausedActivity = true; + break; + case 20061: + _logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId} ] code {code} received, leaving maintenance mode. Reconnecting/Resubscribing socket."); + _ = connection.TriggerReconnectAsync(); // Closing it via socket will automatically reconnect + break; + default: + _logger.Log(LogLevel.Warning, $"[Sckt {connection.SocketId}] unknown info code received: {code}"); + break; + } + + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexSubscription.cs b/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexSubscription.cs new file mode 100644 index 0000000..6573e96 --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexSubscription.cs @@ -0,0 +1,113 @@ +using Bitfinex.Net.Converters; +using Bitfinex.Net.Enums; +using Bitfinex.Net.Objects.Internal; +using Bitfinex.Net.Objects.Sockets.Queries; +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 Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bitfinex.Net.Objects.Sockets.Subscriptions +{ + internal class BitfinexSubscription : Subscription + { + private static readonly MessagePath _1Path = MessagePath.Get().Index(1); + private static readonly MessagePath _10Path = MessagePath.Get().Index(1).Index(0); + private static readonly MessagePath _20Path = MessagePath.Get().Index(2).Index(0); + + private string _channel; + private string? _symbol; + private string? _precision; + private string? _frequency; + private string? _length; + private string? _key; + private int _channelId; + private bool _firstUpdate; + private Action>> _handler; + private Action>? _checksumHandler; + + public override HashSet ListenerIdentifiers { get; set; } = new HashSet(); + + public BitfinexSubscription(ILogger logger, + string channel, + string? symbol, + Action>> handler, + Action>? checksumHandler = null, + bool authenticated = false, + Precision? precision = null, + Frequency? frequency = null, + int? length = null, + string? key = null) + : base(logger, authenticated) + { + _handler = handler; + _checksumHandler = checksumHandler; + _symbol = symbol; + _key = key; + _channel = channel; + _precision = precision == null ? null : JsonConvert.SerializeObject(precision, new PrecisionConverter(false)); + _frequency = frequency == null ? null: JsonConvert.SerializeObject(frequency, new FrequencyConverter(false)); + _length = length?.ToString(); + } + + /// + public override Type? GetMessageType(IMessageAccessor message) + { + var identifier = message.GetValue(_1Path); + + if (identifier == "cs") + return typeof(BitfinexChecksum); + + if (identifier == "hb") + return typeof(BitfinexUpdate); + + if (identifier == null) + { + var nodeType1 = message.GetNodeType(_10Path); + return nodeType1 == NodeType.Array ? typeof(BitfinexUpdate>) : typeof(BitfinexUpdate); + } + + var nodeType = message.GetNodeType(_20Path); + return nodeType == NodeType.Array ? typeof(BitfinexTopicUpdate>) : typeof(BitfinexTopicUpdate); + } + + public override void HandleSubQueryResponse(BitfinexResponse message) + { + _channelId = message.ChannelId!.Value; + _firstUpdate = true; + ListenerIdentifiers = new HashSet { _channelId.ToString() }; + } + + public override Query? GetSubQuery(SocketConnection connection) + { + return new BitfinexSubQuery("subscribe", _channel, _symbol, _precision, _frequency, _length, _key); + } + public override Query? GetUnsubQuery() + { + return new BitfinexUnsubQuery(_channelId); + } + + public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data is BitfinexChecksum checksum) + _checksumHandler?.Invoke(message.As(checksum.Checksum, _symbol)); + else if (message.Data is BitfinexUpdate> arrayUpdate) + _handler?.Invoke(message.As(arrayUpdate.Data, _symbol, _firstUpdate ? SocketUpdateType.Snapshot : SocketUpdateType.Update)); + else if (message.Data is BitfinexUpdate singleUpdate) + _handler?.Invoke(message.As>(new[] { singleUpdate.Data }, _symbol, _firstUpdate ? SocketUpdateType.Snapshot : SocketUpdateType.Update)); + else if (message.Data is BitfinexTopicUpdate> array3Update) + _handler?.Invoke(message.As(array3Update.Data, _symbol, _firstUpdate ? SocketUpdateType.Snapshot : SocketUpdateType.Update)); + else if (message.Data is BitfinexTopicUpdate single3Update) + _handler?.Invoke(message.As>(new[] { single3Update.Data }, _symbol, _firstUpdate ? SocketUpdateType.Snapshot : SocketUpdateType.Update)); + + _firstUpdate = false; + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexUserSubscription.cs b/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexUserSubscription.cs new file mode 100644 index 0000000..c04c30c --- /dev/null +++ b/Bitfinex.Net/Objects/Sockets/Subscriptions/BitfinexUserSubscription.cs @@ -0,0 +1,183 @@ +using Bitfinex.Net.Objects.Models; +using Bitfinex.Net.Objects.Models.Socket; +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 Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Bitfinex.Net.Objects.Sockets.Subscriptions +{ + internal class BitfinexUserSubscription : Subscription + { + private static readonly MessagePath _messagePath = MessagePath.Get().Index(1); + private static readonly MessagePath _marginInfoPath = MessagePath.Get().Index(2).Index(0); + + private readonly Action>>? _positionHandler; + private readonly Action>>? _walletHandler; + private readonly Action>>? _orderHandler; + private readonly Action>>? _fundingOfferHandler; + private readonly Action>>? _fundingCreditHandler; + private readonly Action>>? _fundingLoanHandler; + private readonly Action>? _balanceHandler; + private readonly Action>? _tradeHandler; + private readonly Action>? _fundingTradeHandler; + private readonly Action>? _marginBaseHandler; + private readonly Action>? _marginSymbolHandler; + private readonly Action>? _fundingInfoHandler; + + /// + public override Type? GetMessageType(IMessageAccessor message) + { + var identifier = message.GetValue(_messagePath); + + if (identifier == "hb") + return typeof(BitfinexSocketEvent); + + if (identifier == "ps") + return typeof(BitfinexSocketEvent>); + if (identifier == "pn" || identifier == "pu" || identifier == "pc") + return typeof(BitfinexSocketEvent); + + if (identifier == "bu") + return typeof(BitfinexSocketEvent); + + if (identifier == "miu") + { + var marginInfoType = message.GetValue(_marginInfoPath); + return marginInfoType == "base" ? typeof(BitfinexSocketEvent) : typeof(BitfinexSocketEvent); + } + + if (identifier == "fiu") + return typeof(BitfinexSocketEvent); + + if (identifier == "ws") + return typeof(BitfinexSocketEvent>); + if (identifier == "wu") + return typeof(BitfinexSocketEvent); + + if (identifier == "os") + return typeof(BitfinexSocketEvent>); + if (identifier == "on" || identifier == "ou" || identifier == "oc") + return typeof(BitfinexSocketEvent); + + if (identifier == "te") + return typeof(BitfinexSocketEvent); + if (identifier == "tu") + return typeof(BitfinexSocketEvent); + + if (identifier == "fte") + return typeof(BitfinexSocketEvent); + if (identifier == "ftu") + return typeof(BitfinexSocketEvent); + + if (identifier == "fos") + return typeof(BitfinexSocketEvent>); + if (identifier == "fon" || identifier == "fou" || identifier == "foc") + return typeof(BitfinexSocketEvent); + + if (identifier == "fcs") + return typeof(BitfinexSocketEvent>); + if (identifier == "fcn" || identifier == "fcu" || identifier == "fcc") + return typeof(BitfinexSocketEvent); + + if (identifier == "fls") + return typeof(BitfinexSocketEvent>); + if (identifier == "fln" || identifier == "flu" || identifier == "flc") + return typeof(BitfinexSocketEvent); + + return null; + } + + public override HashSet ListenerIdentifiers { get; set; } = new HashSet() { "0" }; + + public BitfinexUserSubscription(ILogger logger, + Action>>? positionHandler, + Action>>? walletHandler, + Action>>? orderHandler, + Action>>? fundingOfferHandler, + Action>>? fundingCreditHandler, + Action>>? fundingLoanHandler, + Action>? balanceHandler, + Action>? tradeHandler, + Action>? fundingTradeHandler, + Action>? fundingInfoHandler, + Action>? marginBaseHandler, + Action>? marginSymbolHandler + ) + : base(logger, true) + { + _positionHandler = positionHandler; + _walletHandler = walletHandler; + _orderHandler = orderHandler; + _fundingOfferHandler = fundingOfferHandler; + _fundingCreditHandler = fundingCreditHandler; + _fundingLoanHandler = fundingLoanHandler; + _balanceHandler = balanceHandler; + _tradeHandler = tradeHandler; + _fundingTradeHandler = fundingTradeHandler; + _fundingInfoHandler = fundingInfoHandler; + _marginBaseHandler = marginBaseHandler; + _marginSymbolHandler = marginSymbolHandler; + } + + public override Query? GetSubQuery(SocketConnection connection) => null; + + public override Query? GetUnsubQuery() => null; + + public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + { + if (message.Data is BitfinexSocketEvent> positionSnapshot) + _positionHandler?.Invoke(message.As>(positionSnapshot.Data)); + else if (message.Data is BitfinexSocketEvent positionUpdate) + _positionHandler?.Invoke(message.As>(new[] { positionUpdate.Data })); + + else if (message.Data is BitfinexSocketEvent> loanSnapshot) + _fundingLoanHandler?.Invoke(message.As>(loanSnapshot.Data)); + else if (message.Data is BitfinexSocketEvent loanUpdate) + _fundingLoanHandler?.Invoke(message.As>(new[] { loanUpdate.Data })); + + else if (message.Data is BitfinexSocketEvent> creditSnapshot) + _fundingCreditHandler?.Invoke(message.As>(creditSnapshot.Data)); + else if (message.Data is BitfinexSocketEvent creditUpdate) + _fundingCreditHandler?.Invoke(message.As>(new[] { creditUpdate.Data })); + + else if (message.Data is BitfinexSocketEvent> offerSnapshot) + _fundingOfferHandler?.Invoke(message.As>(offerSnapshot.Data)); + else if (message.Data is BitfinexSocketEvent offerUpdate) + _fundingOfferHandler?.Invoke(message.As>(new[] { offerUpdate.Data })); + + else if (message.Data is BitfinexSocketEvent> orderSnapshot) + _orderHandler?.Invoke(message.As>(orderSnapshot.Data)); + else if (message.Data is BitfinexSocketEvent orderUpdate) + _orderHandler?.Invoke(message.As>(new[] { orderUpdate.Data })); + + else if (message.Data is BitfinexSocketEvent fundingTrade) + _fundingTradeHandler?.Invoke(message.As(fundingTrade.Data)); + + else if (message.Data is BitfinexSocketEvent trade) + _tradeHandler?.Invoke(message.As(trade.Data)); + + else if (message.Data is BitfinexSocketEvent> walletSnapshot) + _walletHandler?.Invoke(message.As>(walletSnapshot.Data)); + else if (message.Data is BitfinexSocketEvent walletUpdate) + _walletHandler?.Invoke(message.As>(new[] { walletUpdate.Data })); + + else if (message.Data is BitfinexSocketEvent balanceUpdate) + _balanceHandler?.Invoke(message.As(balanceUpdate.Data)); + else if (message.Data is BitfinexSocketEvent fundingInfoUpdate) + _fundingInfoHandler?.Invoke(message.As(fundingInfoUpdate.Data)); + + else if (message.Data is BitfinexSocketEvent marginBaseUpdate) + _marginBaseHandler?.Invoke(message.As(marginBaseUpdate.Data)); + else if (message.Data is BitfinexSocketEvent marginSymbolUpdate) + _marginSymbolHandler?.Invoke(message.As(marginSymbolUpdate.Data)); + + return Task.FromResult(new CallResult(null)); + } + } +} diff --git a/Bitfinex.Net/SymbolOrderBooks/BitfinexSymbolOrderBook.cs b/Bitfinex.Net/SymbolOrderBooks/BitfinexSymbolOrderBook.cs index e55400d..8fa1537 100644 --- a/Bitfinex.Net/SymbolOrderBooks/BitfinexSymbolOrderBook.cs +++ b/Bitfinex.Net/SymbolOrderBooks/BitfinexSymbolOrderBook.cs @@ -6,14 +6,14 @@ using System.Threading.Tasks; using Bitfinex.Net.Clients; using Bitfinex.Net.Enums; +using Bitfinex.Net.ExtensionMethods; using Bitfinex.Net.Interfaces.Clients; -using Bitfinex.Net.Objects; using Bitfinex.Net.Objects.Models; using Bitfinex.Net.Objects.Options; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.OrderBook; -using CryptoExchange.Net.Sockets; using Force.Crc32; using Microsoft.Extensions.Logging; @@ -71,7 +71,7 @@ public BitfinexSymbolOrderBook(string symbol, /// protected override async Task> DoStartAsync(CancellationToken ct) { - if(_precision == Precision.R0) + if (_precision == Precision.R0) throw new ArgumentException("Invalid precision: R0"); var result = await _socketClient.SpotApi.SubscribeToOrderBookUpdatesAsync(Symbol, _precision, Frequency.Realtime, Levels!.Value, ProcessUpdate, ProcessChecksum).ConfigureAwait(false); @@ -85,7 +85,7 @@ protected override async Task> DoStartAsync(Cance } Status = OrderBookStatus.Syncing; - + var setResult = await WaitForSetOrderBookAsync(_initialDataTimeout, ct).ConfigureAwait(false); return setResult ? result : new CallResult(setResult.Error!); } @@ -136,7 +136,9 @@ private void ProcessUpdate(DataEvent> data) askEntries.Add(entry); } else + { bidEntries.Add(entry); + } } } @@ -173,7 +175,9 @@ protected override bool DoChecksum(int checksum) checksumValues.Add(bid.RawQuantity); } else + { _logger.Log(LogLevel.Trace, $"Skipping checksum bid level {i}, no data"); + } if (_asks.Count > i) { @@ -182,7 +186,9 @@ protected override bool DoChecksum(int checksum) checksumValues.Add(ask.RawQuantity); } else + { _logger.Log(LogLevel.Trace, $"Skipping checksum ask level {i}, no data"); + } } var checksumString = string.Join(":", checksumValues); var ourChecksumUtf = (int)Crc32Algorithm.Compute(Encoding.UTF8.GetBytes(checksumString)); diff --git a/README.md b/README.md index 8b11502..cb69679 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,85 @@ -# Bitfinex.Net -[![.NET](https://github.com/JKorf/Bitfinex.Net/actions/workflows/dotnet.yml/badge.svg)](https://github.com/JKorf/Bitfinex.Net/actions/workflows/dotnet.yml) [![Nuget version](https://img.shields.io/nuget/v/bitfinex.net.svg)](https://www.nuget.org/packages/Bitfinex.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/Bitfinex.Net.svg)](https://www.nuget.org/packages/Bitfinex.Net) +# ![.Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net/blob/master/Bitfinex.Net/Icon/icon.png?raw=true) Bitfinex.Net + +[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/Bitfinex.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/Bitfinex.Net/actions/workflows/dotnet.yml) ![License](https://img.shields.io/github/license/JKorf/Bitfinex.Net?style=for-the-badge) Bitfinex.Net is a wrapper around the Bitfinex API as described on [Bitfinex](https://docs.bitfinex.com/docs), 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/Bitfinex.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/bitfinex.net.svg?style=for-the-badge)](https://www.nuget.org/packages/Bitfinex.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/Bitfinex.Net.svg?style=for-the-badge)](https://www.nuget.org/packages/Bitfinex.Net) + + dotnet add package Bitfinex.Net + +## How to use +* REST Endpoints + ```csharp + // Get the ETH/USDT ticker via rest request + var restClient = new BitfinexRestClient(); + var tickerResult = await restClient.SpotApi.ExchangeData.GetTickerAsync("tETHUST"); + var lastPrice = tickerResult.Data.LastPrice; + ``` +* Websocket streams + ```csharp + // Subscribe to ETH/USDT ticker updates via the websocket API + var socketClient = new BitfinexSocketClient(); + var tickerSubscriptionResult = socketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHUST", (update) => + { + var lastPrice = update.Data.LastPrice; + }); + ``` + +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 +Bitfinex.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)| +|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/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)| +|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square)](https://www.nuget.org/packages/Huobi.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/Bitfinex.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 Bitfinex.Net` +## Supported functionality +|API|Supported|Location| +|--|--:|--| +|Rest Public Endpoints|✓|`restClient.SpotApi.ExchangeData`| +|Rest Public Pulse Endpoints|X|| +|Calculation Endpoints|✓|`restClient.SpotApi.ExchangeData`| +|Rest Authenticated Endpoints|✓|`restClient.SpotApi.Account` / `restClient.SpotApi.Trading`| +|Rest Authenticated Pulse Endpoints|X|| +|Rest Authenticated Merchant Endpoints|X|| +|Websocket Public Channels|✓|`socketClient.SpotApi`| +|Websocket Authenticated Channels|✓|`socketClient.SpotApi`| +|Websocket Authenticated Inputs|✓|`socketClient.SpotApi`| ## 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 Bitfinex. This doesn't cost you a thing! -[Link](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO) - ### 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 +89,17 @@ 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 7.1.0-beta2 - 17 Feb 2024 + * Fixed socket authentication + +* Version 7.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 + * Combined multiple private websocket subscriptions into single subscription + * Fixed issue in DI registration causing http client to not be correctly injected + * Removed excessive constructor overload for BitfinexRestClient + * Removed UpdateType from BitfinexTradeSimple model in favor of the UpdateType in the DataEvent wrapper + * Version 7.0.5 - 03 Dec 2023 * Updated CryptoExchange.Net