From 88b02b0b411800a5d1fd73f48895d7ad44ed6ea6 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Thu, 29 Aug 2019 09:30:17 +0200 Subject: [PATCH 01/41] Added array serialization option, adjusted array converter --- .../Attributes/JsonConversionAttribute.cs | 11 ++++++++ .../Converters/ArrayConverter.cs | 16 ++++++++++- CryptoExchange.Net/CryptoExchange.Net.xml | 28 ++++++++++++++++++- CryptoExchange.Net/ExtensionMethods.cs | 13 +++++++-- CryptoExchange.Net/Objects/Enums.cs | 15 ++++++++++ CryptoExchange.Net/RestClient.cs | 7 ++++- CryptoExchange.Net/SocketClient.cs | 3 +- 7 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 CryptoExchange.Net/Attributes/JsonConversionAttribute.cs diff --git a/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs new file mode 100644 index 0000000..67c9021 --- /dev/null +++ b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace CryptoExchange.Net.Attributes +{ + /// + /// Used for conversion in ArrayConverter + /// + public class JsonConversionAttribute: Attribute + { + } +} diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs index 62cebc6..6a8d5e1 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/ArrayConverter.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Globalization; using System.Linq; using System.Reflection; +using CryptoExchange.Net.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -74,7 +75,20 @@ namespace CryptoExchange.Net.Converters } var converterAttribute = (JsonConverterAttribute)property.GetCustomAttribute(typeof(JsonConverterAttribute)) ?? (JsonConverterAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConverterAttribute)); - var value = converterAttribute != null ? arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer { Converters = { (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType) } }) : arr[attribute.Index]; + var conversionAttribute = (JsonConversionAttribute)property.GetCustomAttribute(typeof(JsonConversionAttribute)) ?? (JsonConversionAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConversionAttribute)); + object value = null; + if (converterAttribute != null) + { + value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); + } + else if (conversionAttribute != null) + { + value = arr[attribute.Index].ToObject(property.PropertyType); + } + else + { + value = arr[attribute.Index]; + } if (value != null && property.PropertyType.IsInstanceOfType(value)) property.SetValue(result, value); diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index fd9367b..4a27286 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -4,6 +4,11 @@ CryptoExchange.Net + + + Used for conversion in ArrayConverter + + Marks property as optional @@ -446,12 +451,13 @@ - + Create a query string of the specified parameters The parameters to use Whether or not the values should be url encoded + How to serialize array parameters @@ -1182,6 +1188,21 @@ Bid + + + Define how array parameters should be send + + + + + Send multiple key=value for each entry + + + + + Create an []=value array + + Base class for errors @@ -1846,6 +1867,11 @@ Request body content type + + + How to serialize array parameters + + Timeout for requests diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index f2485a8..cd80a5c 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -8,6 +8,7 @@ using System.Security; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Logging; +using CryptoExchange.Net.Objects; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -69,14 +70,22 @@ namespace CryptoExchange.Net /// /// The parameters to use /// Whether or not the values should be url encoded + /// How to serialize array parameters /// - public static string CreateParamString(this Dictionary parameters, bool urlEncodeValues) + public static string CreateParamString(this Dictionary parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) { var uriString = ""; var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); foreach (var arrayEntry in arraysParameters) { - uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; + if(serializationType == ArrayParametersSerialization.Array) + uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; + else + { + var array = (Array)arrayEntry.Value; + uriString += string.Join("&", array.OfType().Select(a => $"{arrayEntry.Key}={WebUtility.UrlEncode(a.ToString())}")); + uriString += "&"; + } } uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? WebUtility.UrlEncode(s.Value.ToString()) : s.Value)}"))}"; diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs index fc6b630..90c06ed 100644 --- a/CryptoExchange.Net/Objects/Enums.cs +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -82,4 +82,19 @@ /// Bid } + + /// + /// Define how array parameters should be send + /// + public enum ArrayParametersSerialization + { + /// + /// Send multiple key=value for each entry + /// + MultipleValues, + /// + /// Create an []=value array + /// + Array + } } diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 844607f..88fe5eb 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -39,6 +39,11 @@ namespace CryptoExchange.Net /// protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json; + /// + /// How to serialize array parameters + /// + protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array; + /// /// Timeout for requests /// @@ -222,7 +227,7 @@ namespace CryptoExchange.Net parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed); if((method == Constants.GetMethod || method == Constants.DeleteMethod || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true) - uriString += "?" + parameters.CreateParamString(true); + uriString += "?" + parameters.CreateParamString(true, arraySerialization); var request = RequestFactory.Create(uriString); request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index cc53894..9558e73 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -175,7 +175,6 @@ namespace CryptoExchange.Net await socket.Close(handler).ConfigureAwait(false); return new CallResult(null, subResult.Error); } - } else handler.Confirmed = true; @@ -554,7 +553,7 @@ namespace CryptoExchange.Net /// public virtual async Task UnsubscribeAll() { - log.Write(LogVerbosity.Debug, $"Closing all {sockets.Count} subscriptions"); + log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.handlers.Count(h => h.UserSubscription))} subscriptions"); await Task.Run(() => { From f1a2372ddb6815351b70e8502c60d580b6890688 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Thu, 29 Aug 2019 09:30:35 +0200 Subject: [PATCH 02/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 4 ++-- README.md | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 51025d2..a7744fa 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -5,13 +5,13 @@ CryptoExchange.Net JKorf - 2.1.7 + 2.1.8 false https://github.com/JKorf/CryptoExchange.Net https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE en true - 2.1.7 - Fixed bug with socket connection not being disposed after lost connection, Resubscribing after reconnecting socket now in parallel + 2.1.8 - Added array serialization options for implementations CryptoExchange.Net.xml diff --git a/README.md b/README.md index 760c1d5..6fbea0e 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,9 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 2.1.8 - 29 Aug 2019 + * Added array serialization options for implementations + * Version 2.1.7 - 07 Aug 2019 * Fixed bug with socket connection not being disposed after lost connection * Resubscribing after reconnecting socket now in parallel From 33cc61d1d6ff2123db95b0371c0d54eb883059eb Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Thu, 29 Aug 2019 11:37:11 +0200 Subject: [PATCH 03/41] Added kraken ref --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6fbea0e..3f51150 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ A base library for easy implementation of cryptocurrency API's. Include:
Kucoin + +
+Kraken + From 8de3464ea6eb327684fdc16ace02e45cff89c96f Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Thu, 29 Aug 2019 11:57:12 +0200 Subject: [PATCH 04/41] Removed kraken from todo list --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 3f51150..988e4ce 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,6 @@ Planned implementations (no timeline or specific order): * BitMEX * Bitstamp * CoinFalcon -* Kraken * Binance DEX ## Donations From c01bcc87b13d5d663d6cff09257c95b99e3f2da5 Mon Sep 17 00:00:00 2001 From: msg_kurt Date: Wed, 4 Sep 2019 12:48:20 +0300 Subject: [PATCH 05/41] added orderbook update event --- CryptoExchange.Net/OrderBook/SymbolOrderBook.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index e6c2e29..5edd59d 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -69,8 +69,11 @@ namespace CryptoExchange.Net.OrderBook /// /// Event when the state changes /// - public event Action OnStatusChange; - + public event Action OnStatusChange; + /// + /// Event when orderbook was updated + /// + public event Action OnOrderBookUpdate; /// /// The number of asks in the book /// @@ -259,6 +262,7 @@ namespace CryptoExchange.Net.OrderBook CheckProcessBuffer(); bookSet = true; + OnOrderBookUpdate?.Invoke(); log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } @@ -299,6 +303,7 @@ namespace CryptoExchange.Net.OrderBook ProcessUpdate(entry.Type, entry.Entry); LastSequenceNumber = lastSequenceNumber; CheckProcessBuffer(); + OnOrderBookUpdate?.Invoke(); log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update: {entries.Count} entries processed"); } } From 43c17bae64339d4c32aecbf38daf1e8c749bb2f7 Mon Sep 17 00:00:00 2001 From: msg_kurt Date: Wed, 4 Sep 2019 16:49:32 +0300 Subject: [PATCH 06/41] added timeout for triggering order book updating event --- CryptoExchange.Net/Objects/Options.cs | 18 +++++--- .../OrderBook/SymbolOrderBook.cs | 43 +++++++++++++------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 525f1c8..156364f 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -26,8 +26,12 @@ namespace CryptoExchange.Net.Objects /// /// Base for order book options /// - public class OrderBookOptions: BaseOptions + public class OrderBookOptions : BaseOptions { + /// + /// Update event raising timeout in milliseconds (to limit it at high-liquidity order books) + /// + public int UpdateEventTimeout { get; } /// /// The name of the order book implementation /// @@ -42,8 +46,10 @@ namespace CryptoExchange.Net.Objects /// /// The name of the order book implementation /// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. - public OrderBookOptions(string name, bool sequencesAreConsecutive) + /// Update event raising timeout in milliseconds (to limit it at high-liquidity order books) + public OrderBookOptions(string name, bool sequencesAreConsecutive, int? updateInterval) { + UpdateEventTimeout = updateInterval ?? 1000; OrderBookName = name; SequenceNumbersAreConsecutive = sequencesAreConsecutive; } @@ -52,7 +58,7 @@ namespace CryptoExchange.Net.Objects /// /// Base client options /// - public class ClientOptions: BaseOptions + public class ClientOptions : BaseOptions { /// @@ -74,7 +80,7 @@ namespace CryptoExchange.Net.Objects /// /// Base for rest client options /// - public class RestClientOptions: ClientOptions + public class RestClientOptions : ClientOptions { /// /// List of rate limiters to use @@ -96,7 +102,7 @@ namespace CryptoExchange.Net.Objects /// /// /// - public T Copy() where T:RestClientOptions, new() + public T Copy() where T : RestClientOptions, new() { var copy = new T { @@ -119,7 +125,7 @@ namespace CryptoExchange.Net.Objects /// /// Base for socket client options /// - public class SocketClientOptions: ClientOptions + public class SocketClientOptions : ClientOptions { /// /// Whether or not the socket should automatically reconnect when losing connection diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 5edd59d..2e5f2dc 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -13,7 +13,7 @@ namespace CryptoExchange.Net.OrderBook /// /// Base for order book implementations /// - public abstract class SymbolOrderBook: IDisposable + public abstract class SymbolOrderBook : IDisposable { /// /// The process buffer, used while syncing @@ -27,6 +27,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The bid list /// + protected SortedList bids; private OrderBookStatus status; private UpdateSubscription subscription; @@ -69,12 +70,19 @@ namespace CryptoExchange.Net.OrderBook /// /// Event when the state changes /// - public event Action OnStatusChange; + public event Action OnStatusChange; /// - /// Event when orderbook was updated - /// + /// Event when orderbook was updated, but not more often then timeout setted in orderbook options (1000ms by default). Be careful! with small timeout it can generate a lot of events at high-liquidity order books + /// public event Action OnOrderBookUpdate; /// + /// Should be useful for low-liquidity order-books to monitor market activity + /// + public DateTime LastOrderBookUpdate; + private DateTime LastOrderBookUpdateEventTrigger; + + private readonly int updateEventInterval; + /// /// The number of asks in the book /// public int AskCount { get; private set; } @@ -141,6 +149,7 @@ namespace CryptoExchange.Net.OrderBook id = options.OrderBookName; processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; + updateEventInterval = options.UpdateEventTimeout; Symbol = symbol; Status = OrderBookStatus.Disconnected; @@ -166,7 +175,7 @@ namespace CryptoExchange.Net.OrderBook { Status = OrderBookStatus.Connecting; var startResult = await DoStart().ConfigureAwait(false); - if(!startResult.Success) + if (!startResult.Success) return new CallResult(false, startResult.Error); subscription = startResult.Data; @@ -234,7 +243,7 @@ namespace CryptoExchange.Net.OrderBook /// /// protected abstract Task> DoResync(); - + /// /// Set the initial data for the order book /// @@ -249,7 +258,7 @@ namespace CryptoExchange.Net.OrderBook return; asks.Clear(); - foreach(var ask in askList) + foreach (var ask in askList) asks.Add(ask.Price, new OrderBookEntry(ask.Price, ask.Quantity)); bids.Clear(); foreach (var bid in bidList) @@ -262,7 +271,12 @@ namespace CryptoExchange.Net.OrderBook CheckProcessBuffer(); bookSet = true; - OnOrderBookUpdate?.Invoke(); + LastOrderBookUpdate = DateTime.UtcNow; + if ((LastOrderBookUpdate - LastOrderBookUpdateEventTrigger).TotalMilliseconds >= updateEventInterval) + { + OnOrderBookUpdate?.Invoke(); + LastOrderBookUpdateEventTrigger = DateTime.UtcNow; + } log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } @@ -299,11 +313,16 @@ namespace CryptoExchange.Net.OrderBook } else { - foreach(var entry in entries) + foreach (var entry in entries) ProcessUpdate(entry.Type, entry.Entry); LastSequenceNumber = lastSequenceNumber; CheckProcessBuffer(); - OnOrderBookUpdate?.Invoke(); + LastOrderBookUpdate = DateTime.UtcNow; + if ((LastOrderBookUpdate - LastOrderBookUpdateEventTrigger).TotalMilliseconds >= updateEventInterval) + { + OnOrderBookUpdate?.Invoke(); + LastOrderBookUpdateEventTrigger = DateTime.UtcNow; + } log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update: {entries.Count} entries processed"); } } @@ -316,7 +335,7 @@ namespace CryptoExchange.Net.OrderBook { foreach (var bufferEntry in processBuffer.OrderBy(b => b.FirstSequence).ToList()) { - if(bufferEntry.LastSequence < LastSequenceNumber) + if (bufferEntry.LastSequence < LastSequenceNumber) { processBuffer.Remove(bufferEntry); continue; @@ -325,7 +344,7 @@ namespace CryptoExchange.Net.OrderBook if (bufferEntry.FirstSequence > LastSequenceNumber + 1) break; - foreach(var entry in bufferEntry.Entries) + foreach (var entry in bufferEntry.Entries) ProcessUpdate(entry.Type, entry.Entry); processBuffer.Remove(bufferEntry); LastSequenceNumber = bufferEntry.LastSequence; From 6a3df9f7e79327113a2b3a48203443e6a3e9fcdd Mon Sep 17 00:00:00 2001 From: msg_kurt Date: Wed, 4 Sep 2019 17:23:30 +0300 Subject: [PATCH 07/41] removed event triggering timeout --- CryptoExchange.Net/CryptoExchange.Net.xml | 10 ++++++++++ CryptoExchange.Net/Objects/Options.cs | 12 +++--------- CryptoExchange.Net/OrderBook/SymbolOrderBook.cs | 17 +++-------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index fd9367b..63ed486 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -1553,6 +1553,16 @@ Event when the state changes + + + Event when orderbook was updated. Be careful! It can generate a lot of events at high-liquidity markets + + + + + Should be useful for low-liquidity order-books to monitor market activity + + The number of asks in the book diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 156364f..22c3729 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -27,11 +27,7 @@ namespace CryptoExchange.Net.Objects /// Base for order book options /// public class OrderBookOptions : BaseOptions - { - /// - /// Update event raising timeout in milliseconds (to limit it at high-liquidity order books) - /// - public int UpdateEventTimeout { get; } + { /// /// The name of the order book implementation /// @@ -46,10 +42,8 @@ namespace CryptoExchange.Net.Objects /// /// The name of the order book implementation /// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. - /// Update event raising timeout in milliseconds (to limit it at high-liquidity order books) - public OrderBookOptions(string name, bool sequencesAreConsecutive, int? updateInterval) - { - UpdateEventTimeout = updateInterval ?? 1000; + public OrderBookOptions(string name, bool sequencesAreConsecutive) + { OrderBookName = name; SequenceNumbersAreConsecutive = sequencesAreConsecutive; } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 2e5f2dc..d942ed9 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -72,16 +72,14 @@ namespace CryptoExchange.Net.OrderBook /// public event Action OnStatusChange; /// - /// Event when orderbook was updated, but not more often then timeout setted in orderbook options (1000ms by default). Be careful! with small timeout it can generate a lot of events at high-liquidity order books + /// Event when orderbook was updated. Be careful! It can generate a lot of events at high-liquidity markets /// public event Action OnOrderBookUpdate; /// /// Should be useful for low-liquidity order-books to monitor market activity /// public DateTime LastOrderBookUpdate; - private DateTime LastOrderBookUpdateEventTrigger; - private readonly int updateEventInterval; /// /// The number of asks in the book /// @@ -149,7 +147,6 @@ namespace CryptoExchange.Net.OrderBook id = options.OrderBookName; processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; - updateEventInterval = options.UpdateEventTimeout; Symbol = symbol; Status = OrderBookStatus.Disconnected; @@ -272,11 +269,7 @@ namespace CryptoExchange.Net.OrderBook CheckProcessBuffer(); bookSet = true; LastOrderBookUpdate = DateTime.UtcNow; - if ((LastOrderBookUpdate - LastOrderBookUpdateEventTrigger).TotalMilliseconds >= updateEventInterval) - { - OnOrderBookUpdate?.Invoke(); - LastOrderBookUpdateEventTrigger = DateTime.UtcNow; - } + OnOrderBookUpdate?.Invoke(); log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } @@ -318,11 +311,7 @@ namespace CryptoExchange.Net.OrderBook LastSequenceNumber = lastSequenceNumber; CheckProcessBuffer(); LastOrderBookUpdate = DateTime.UtcNow; - if ((LastOrderBookUpdate - LastOrderBookUpdateEventTrigger).TotalMilliseconds >= updateEventInterval) - { - OnOrderBookUpdate?.Invoke(); - LastOrderBookUpdateEventTrigger = DateTime.UtcNow; - } + OnOrderBookUpdate?.Invoke(); log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update: {entries.Count} entries processed"); } } From 8cb34205204f1ba338fee9dd95e667d0f5a2d874 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Thu, 5 Sep 2019 11:00:57 +0200 Subject: [PATCH 08/41] Cleanup/Fixed Resharper comments --- .../Authentication/ApiCredentials.cs | 4 +- CryptoExchange.Net/BaseClient.cs | 10 +- .../Converters/ArrayConverter.cs | 2 +- .../Converters/BaseConverter.cs | 2 +- .../TimestampNanoSecondsConverter.cs | 4 +- CryptoExchange.Net/CryptoExchange.Net.xml | 137 +++++++++++++++--- CryptoExchange.Net/ExtensionMethods.cs | 4 +- CryptoExchange.Net/Interfaces/IRestClient.cs | 1 - .../Interfaces/ISymbolOrderBook.cs | 93 ++++++++++++ .../ISymbolOrderBookEntry.cs | 2 +- .../Objects/ByteOrderComparer.cs | 4 +- CryptoExchange.Net/Objects/CallResult.cs | 4 +- CryptoExchange.Net/Objects/Enums.cs | 2 +- .../OrderBook/OrderBookEntry.cs | 4 +- CryptoExchange.Net/OrderBook/ProcessEntry.cs | 3 +- .../OrderBook/SymbolOrderBook.cs | 43 +++--- .../RateLimiter/RateLimiterAPIKey.cs | 2 +- CryptoExchange.Net/Requests/Request.cs | 2 +- CryptoExchange.Net/RestClient.cs | 33 +++-- CryptoExchange.Net/SocketClient.cs | 85 +++++------ CryptoExchange.Net/Sockets/BaseSocket.cs | 6 +- .../Sockets/SocketConnection.cs | 4 +- 22 files changed, 315 insertions(+), 136 deletions(-) create mode 100644 CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs rename CryptoExchange.Net/{OrderBook => Interfaces}/ISymbolOrderBookEntry.cs (89%) diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 6b2d56a..e6f1dcb 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -14,12 +14,12 @@ namespace CryptoExchange.Net.Authentication /// /// The api key to authenticate requests /// - public SecureString Key { get; private set; } + public SecureString Key { get; } /// /// The api secret to authenticate requests /// - public SecureString Secret { get; private set; } + public SecureString Secret { get; } /// /// The private key to authenticate requests diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 6d48158..b4e79ed 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -140,7 +140,7 @@ namespace CryptoExchange.Net protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer serializer = null) { var tokenResult = ValidateJson(data); - return !tokenResult.Success ? new CallResult(default(T), tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer); + return !tokenResult.Success ? new CallResult(default, tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer); } /// @@ -184,19 +184,19 @@ namespace CryptoExchange.Net { var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {obj}"; log.Write(LogVerbosity.Error, info); - return new CallResult(default(T), new DeserializeError(info)); + return new CallResult(default, new DeserializeError(info)); } catch (JsonSerializationException jse) { var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {obj}"; log.Write(LogVerbosity.Error, info); - return new CallResult(default(T), new DeserializeError(info)); + return new CallResult(default, new DeserializeError(info)); } catch (Exception ex) { var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {obj}"; log.Write(LogVerbosity.Error, info); - return new CallResult(default(T), new DeserializeError(info)); + return new CallResult(default, new DeserializeError(info)); } } @@ -277,7 +277,7 @@ namespace CryptoExchange.Net var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault(); if (attr == null) { - if (String.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase)) + if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase)) return prop; } else diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs index 6a8d5e1..ce6dede 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/ArrayConverter.cs @@ -76,7 +76,7 @@ namespace CryptoExchange.Net.Converters var converterAttribute = (JsonConverterAttribute)property.GetCustomAttribute(typeof(JsonConverterAttribute)) ?? (JsonConverterAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConverterAttribute)); var conversionAttribute = (JsonConversionAttribute)property.GetCustomAttribute(typeof(JsonConversionAttribute)) ?? (JsonConversionAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConversionAttribute)); - object value = null; + object value; if (converterAttribute != null) { value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index 47ce489..b458149 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -78,7 +78,7 @@ namespace CryptoExchange.Net.Converters return true; } - result = default(T); + result = default; return false; } diff --git a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs index 17a6803..fb8205d 100644 --- a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs @@ -8,6 +8,8 @@ namespace CryptoExchange.Net.Converters /// public class TimestampNanoSecondsConverter : JsonConverter { + private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; + /// public override bool CanConvert(Type objectType) { @@ -20,7 +22,6 @@ namespace CryptoExchange.Net.Converters if (reader.Value == null) return null; - var ticksPerNanosecond = (TimeSpan.TicksPerMillisecond / 1000m / 1000); var nanoSeconds = long.Parse(reader.Value.ToString()); return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerNanosecond)); } @@ -28,7 +29,6 @@ namespace CryptoExchange.Net.Converters /// public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var ticksPerNanosecond = (TimeSpan.TicksPerMillisecond / 1000m / 1000); writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).Ticks / ticksPerNanosecond)); } } diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 0a2f54e..482e013 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -469,7 +469,7 @@ - Header collection to inenumerable + Header collection to IEnumerable @@ -709,6 +709,110 @@ + + + Interface for order book + + + + + The status of the order book. Order book is up to date when the status is `Synced` + + + + + Last update identifier + + + + + The symbol of the order book + + + + + Event when the state changes + + + + + Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + + + + + Timestamp of the last update + + + + + The number of asks in the book + + + + + The number of bids in the book + + + + + The list of asks + + + + + The list of bids + + + + + The best bid currently in the order book + + + + + The best ask currently in the order book + + + + + Start connecting and synchronizing the order book + + + + + + Start connecting and synchronizing the order book + + + + + + Stop syncing the order book + + + + + + Stop syncing the order book + + + + + + Interface for order book entries + + + + + The quantity of the entry + + + + + The price of the entry + + Interface for websocket interaction @@ -1445,21 +1549,6 @@ - - - Interface for order book entries - - - - - The quantity of the entry - - - - - The price of the entry - - Order book entry @@ -1522,7 +1611,7 @@ The type - + ctor @@ -1576,12 +1665,12 @@ - Event when orderbook was updated. Be careful! It can generate a lot of events at high-liquidity markets + Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets - + - Should be useful for low-liquidity order-books to monitor market activity + Timestamp of the last update @@ -1662,7 +1751,7 @@ - + Set the initial data for the order book @@ -1683,7 +1772,7 @@ Check and empty the process buffer; see what entries to update the book with - + Update order book with an entry @@ -1789,7 +1878,7 @@ - Create request object for webrequest + Create request object for web request @@ -2126,7 +2215,7 @@ Query for data - Exepected result type + Expected result type The request to send Whether the socket should be authenticated diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index cd80a5c..8d2d2c2 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -127,7 +127,7 @@ namespace CryptoExchange.Net } /// - /// Header collection to inenumerable + /// Header collection to IEnumerable /// /// /// @@ -153,7 +153,7 @@ namespace CryptoExchange.Net public static async Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken) { RegisteredWaitHandle registeredHandle = null; - CancellationTokenRegistration tokenRegistration = default(CancellationTokenRegistration); + CancellationTokenRegistration tokenRegistration = default; try { var tcs = new TaskCompletionSource(); diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index b3a8c4b..c2f3b82 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs new file mode 100644 index 0000000..ad09904 --- /dev/null +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CryptoExchange.Net.Objects; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Interface for order book + /// + public interface ISymbolOrderBook + { + /// + /// The status of the order book. Order book is up to date when the status is `Synced` + /// + OrderBookStatus Status { get; set; } + + /// + /// Last update identifier + /// + long LastSequenceNumber { get; } + /// + /// The symbol of the order book + /// + string Symbol { get; } + + /// + /// Event when the state changes + /// + event Action OnStatusChange; + /// + /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + /// + event Action OnOrderBookUpdate; + /// + /// Timestamp of the last update + /// + DateTime LastOrderBookUpdate { get; } + + /// + /// The number of asks in the book + /// + int AskCount { get; } + /// + /// The number of bids in the book + /// + int BidCount { get; } + + /// + /// The list of asks + /// + IEnumerable Asks { get; } + + /// + /// The list of bids + /// + IEnumerable Bids { get; } + + /// + /// The best bid currently in the order book + /// + ISymbolOrderBookEntry BestBid { get; } + + /// + /// The best ask currently in the order book + /// + ISymbolOrderBookEntry BestAsk { get; } + + /// + /// Start connecting and synchronizing the order book + /// + /// + CallResult Start(); + + /// + /// Start connecting and synchronizing the order book + /// + /// + Task> StartAsync(); + + /// + /// Stop syncing the order book + /// + /// + void Stop(); + + /// + /// Stop syncing the order book + /// + /// + Task StopAsync(); + } +} diff --git a/CryptoExchange.Net/OrderBook/ISymbolOrderBookEntry.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs similarity index 89% rename from CryptoExchange.Net/OrderBook/ISymbolOrderBookEntry.cs rename to CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs index b5cb8ce..419f41a 100644 --- a/CryptoExchange.Net/OrderBook/ISymbolOrderBookEntry.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.OrderBook +namespace CryptoExchange.Net.Interfaces { /// /// Interface for order book entries diff --git a/CryptoExchange.Net/Objects/ByteOrderComparer.cs b/CryptoExchange.Net/Objects/ByteOrderComparer.cs index 29be971..7ad3cbd 100644 --- a/CryptoExchange.Net/Objects/ByteOrderComparer.cs +++ b/CryptoExchange.Net/Objects/ByteOrderComparer.cs @@ -21,8 +21,8 @@ namespace CryptoExchange.Net.Objects // If one is null and the other isn't, then the // one that is null is "lesser". - if (x == null && y != null) return -1; - if (x != null && y == null) return 1; + if (x == null) return -1; + if (y == null) return 1; // Both arrays are non-null. Find the shorter // of the two lengths. diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index d8c2f59..1085328 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -71,7 +71,7 @@ namespace CryptoExchange.Net.Objects /// public static WebCallResult CreateErrorResult(Error error) { - return new WebCallResult(null, null, default(T), error); + return new WebCallResult(null, null, default, error); } /// @@ -83,7 +83,7 @@ namespace CryptoExchange.Net.Objects /// public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable> responseHeaders, Error error) { - return new WebCallResult(code, responseHeaders, default(T), error); + return new WebCallResult(code, responseHeaders, default, error); } } } diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs index 90c06ed..3af3694 100644 --- a/CryptoExchange.Net/Objects/Enums.cs +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -65,7 +65,7 @@ /// /// Data synced, order book is up to date /// - Synced, + Synced } /// diff --git a/CryptoExchange.Net/OrderBook/OrderBookEntry.cs b/CryptoExchange.Net/OrderBook/OrderBookEntry.cs index 3faccf2..4bf9c39 100644 --- a/CryptoExchange.Net/OrderBook/OrderBookEntry.cs +++ b/CryptoExchange.Net/OrderBook/OrderBookEntry.cs @@ -1,4 +1,6 @@ -namespace CryptoExchange.Net.OrderBook +using CryptoExchange.Net.Interfaces; + +namespace CryptoExchange.Net.OrderBook { /// /// Order book entry diff --git a/CryptoExchange.Net/OrderBook/ProcessEntry.cs b/CryptoExchange.Net/OrderBook/ProcessEntry.cs index a59bc5f..5ebff10 100644 --- a/CryptoExchange.Net/OrderBook/ProcessEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessEntry.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.OrderBook { diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index d942ed9..47671b2 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; @@ -13,7 +14,7 @@ namespace CryptoExchange.Net.OrderBook /// /// Base for order book implementations /// - public abstract class SymbolOrderBook : IDisposable + public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable { /// /// The process buffer, used while syncing @@ -23,12 +24,12 @@ namespace CryptoExchange.Net.OrderBook /// /// The ask list /// - protected SortedList asks; + protected SortedList asks; /// /// The bid list /// - protected SortedList bids; + protected SortedList bids; private OrderBookStatus status; private UpdateSubscription subscription; private readonly bool sequencesAreConsecutive; @@ -72,13 +73,13 @@ namespace CryptoExchange.Net.OrderBook /// public event Action OnStatusChange; /// - /// Event when orderbook was updated. Be careful! It can generate a lot of events at high-liquidity markets + /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets /// public event Action OnOrderBookUpdate; /// - /// Should be useful for low-liquidity order-books to monitor market activity + /// Timestamp of the last update /// - public DateTime LastOrderBookUpdate; + public DateTime LastOrderBookUpdate { get; private set; } /// /// The number of asks in the book @@ -150,8 +151,8 @@ namespace CryptoExchange.Net.OrderBook Symbol = symbol; Status = OrderBookStatus.Disconnected; - asks = new SortedList(); - bids = new SortedList(new DescComparer()); + asks = new SortedList(); + bids = new SortedList(new DescComparer()); log = new Log { Level = options.LogVerbosity }; var writers = options.LogWriters ?? new List { new DebugTextWriter() }; @@ -177,7 +178,7 @@ namespace CryptoExchange.Net.OrderBook subscription = startResult.Data; subscription.ConnectionLost += Reset; - subscription.ConnectionRestored += (time) => Resync(); + subscription.ConnectionRestored += time => Resync(); Status = OrderBookStatus.Synced; return new CallResult(true, null); } @@ -194,7 +195,7 @@ namespace CryptoExchange.Net.OrderBook private void Resync() { Status = OrderBookStatus.Syncing; - bool success = false; + var success = false; while (!success) { if (Status != OrderBookStatus.Syncing) @@ -289,7 +290,7 @@ namespace CryptoExchange.Net.OrderBook if (!bookSet) { - var entry = new ProcessBufferEntry() + var entry = new ProcessBufferEntry { FirstSequence = firstSequenceNumber, LastSequence = lastSequenceNumber, @@ -350,25 +351,25 @@ namespace CryptoExchange.Net.OrderBook var listToChange = type == OrderBookEntryType.Ask ? asks : bids; if (entry.Quantity == 0) { - var bookEntry = listToChange.SingleOrDefault(i => i.Key == entry.Price); - if (!bookEntry.Equals(default(KeyValuePair))) - { - listToChange.Remove(entry.Price); - if (type == OrderBookEntryType.Ask) AskCount--; - else BidCount--; - } + if (!listToChange.ContainsKey(entry.Price)) + return; + + listToChange.Remove(entry.Price); + if (type == OrderBookEntryType.Ask) AskCount--; + else BidCount--; } else { - var bookEntry = listToChange.SingleOrDefault(i => i.Key == entry.Price); - if (bookEntry.Equals(default(KeyValuePair))) + if (!listToChange.ContainsKey(entry.Price)) { listToChange.Add(entry.Price, new OrderBookEntry(entry.Price, entry.Quantity)); if (type == OrderBookEntryType.Ask) AskCount++; else BidCount++; } else - bookEntry.Value.Quantity = entry.Quantity; + { + listToChange[entry.Price].Quantity = entry.Quantity; + } } } diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs index ce8d9e0..2e8ea4f 100644 --- a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs +++ b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs @@ -35,7 +35,7 @@ namespace CryptoExchange.Net.RateLimiter if(client.authProvider?.Credentials == null) return new CallResult(0, null); - string key = client.authProvider.Credentials.Key.GetString(); + var key = client.authProvider.Credentials.Key.GetString(); int waitTime; RateLimitObject rlo; diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 5566699..611f38c 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Requests private readonly WebRequest request; /// - /// Create request object for webrequest + /// Create request object for web request /// /// public Request(WebRequest request) diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 88fe5eb..c73f25b 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -126,14 +126,14 @@ namespace CryptoExchange.Net catch(PingException e) { if (e.InnerException == null) - return new CallResult(0, new CantConnectError() {Message = "Ping failed: " + e.Message}); + return new CallResult(0, new CantConnectError {Message = "Ping failed: " + e.Message}); if (e.InnerException is SocketException exception) - return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + exception.SocketErrorCode }); - return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + e.InnerException.Message }); + return new CallResult(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode }); + return new CallResult(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message }); } - return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError() { Message = "Ping failed: " + reply.Status }); + return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status }); } /// @@ -280,7 +280,7 @@ namespace CryptoExchange.Net } else if(requestBodyFormat == RequestBodyFormat.FormData) { - var formData = HttpUtility.ParseQueryString(String.Empty); + var formData = HttpUtility.ParseQueryString(string.Empty); foreach (var kvp in parameters.OrderBy(p => p.Key)) formData.Add(kvp.Key, kvp.Value.ToString()); var stringData = formData.ToString(); @@ -320,19 +320,24 @@ namespace CryptoExchange.Net try { - using (var reader = new StreamReader(response.GetResponseStream())) + var responseStream = response?.GetResponseStream(); + if (response != null && responseStream != null) { - returnedData = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Warning, "Server returned an error: " + returnedData); + using (var reader = new StreamReader(responseStream)) + { + returnedData = await reader.ReadToEndAsync().ConfigureAwait(false); + log.Write(LogVerbosity.Warning, "Server returned an error: " + returnedData); + } + + response.Close(); + + var jsonResult = ValidateJson(returnedData); + return !jsonResult.Success ? new WebCallResult(statusCode, returnHeaders, null, jsonResult.Error) : new WebCallResult(statusCode, returnHeaders, null, ParseErrorResponse(jsonResult.Data)); } - - response.Close(); - - var jsonResult = ValidateJson(returnedData); - return !jsonResult.Success ? new WebCallResult(statusCode, returnHeaders, null, jsonResult.Error) : new WebCallResult(statusCode, returnHeaders, null, ParseErrorResponse(jsonResult.Data)); } - catch (Exception) + catch (Exception e) { + log.Write(LogVerbosity.Debug, "Not able to read server response: " + e.Message); } var infoMessage = "No response from server"; diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index 9558e73..f2f8afd 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -141,7 +141,7 @@ namespace CryptoExchange.Net { SocketConnection socket; SocketSubscription handler; - bool released = false; + var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { @@ -193,13 +193,7 @@ namespace CryptoExchange.Net protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription) { CallResult callResult = null; - await socket.SendAndWait(request, ResponseTimeout, (data) => - { - if (!HandleSubscriptionResponse(socket, subscription, request, data, out callResult)) - return false; - - return true; - }).ConfigureAwait(false); + await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -210,7 +204,7 @@ namespace CryptoExchange.Net /// /// Query for data /// - /// Exepected result type + /// Expected result type /// The request to send /// Whether the socket should be authenticated /// @@ -230,7 +224,7 @@ namespace CryptoExchange.Net protected virtual async Task> Query(string url, object request, bool authenticated) { SocketConnection socket; - bool released = false; + var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { @@ -244,7 +238,7 @@ namespace CryptoExchange.Net var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); if (!connectResult.Success) - return new CallResult(default(T), connectResult.Error); + return new CallResult(default, connectResult.Error); } finally { @@ -257,7 +251,7 @@ namespace CryptoExchange.Net if (socket.PausedActivity) { log.Write(LogVerbosity.Info, "Socket has been paused, can't send query at this moment"); - return new CallResult(default(T), new ServerError("Socket is paused")); + return new CallResult(default, new ServerError("Socket is paused")); } return await QueryAndWait(socket, request).ConfigureAwait(false); @@ -272,8 +266,8 @@ namespace CryptoExchange.Net /// protected virtual async Task> QueryAndWait(SocketConnection socket, object request) { - CallResult dataResult = new CallResult(default(T), new ServerError("No response on query received")); - await socket.SendAndWait(request, ResponseTimeout, (data) => + var dataResult = new CallResult(default, new ServerError("No response on query received")); + await socket.SendAndWait(request, ResponseTimeout, data => { if (!HandleQueryResponse(socket, request, data, out var callResult)) return false; @@ -293,28 +287,25 @@ namespace CryptoExchange.Net /// protected virtual async Task> ConnectIfNeeded(SocketConnection socket, bool authenticated) { - if (!socket.Connected) + if (socket.Connected) + return new CallResult(true, null); + + var connectResult = await ConnectSocket(socket).ConfigureAwait(false); + if (!connectResult.Success) + return new CallResult(false, new CantConnectError()); + + if (!authenticated || socket.Authenticated) + return new CallResult(true, null); + + var result = await AuthenticateSocket(socket).ConfigureAwait(false); + if (!result.Success) { - var connectResult = await ConnectSocket(socket).ConfigureAwait(false); - if (!connectResult.Success) - { - return new CallResult(false, new CantConnectError()); - } - - if (authenticated && !socket.Authenticated) - { - var result = await AuthenticateSocket(socket).ConfigureAwait(false); - if (!result.Success) - { - log.Write(LogVerbosity.Warning, "Socket authentication failed"); - result.Error.Message = "Authentication failed: " + result.Error.Message; - return new CallResult(false, result.Error); - } - - socket.Authenticated = true; - } + log.Write(LogVerbosity.Warning, "Socket authentication failed"); + result.Error.Message = "Authentication failed: " + result.Error.Message; + return new CallResult(false, result.Error); } + socket.Authenticated = true; return new CallResult(true, null); } @@ -388,11 +379,11 @@ namespace CryptoExchange.Net /// protected virtual SocketSubscription AddHandler(object request, string identifier, bool userSubscription, SocketConnection connection, Action dataHandler) { - Action internalHandler = (socketWrapper, data) => + void InternalHandler(SocketConnection socketWrapper, JToken data) { if (typeof(T) == typeof(string)) { - dataHandler((T)Convert.ChangeType(data.ToString(), typeof(T))); + dataHandler((T) Convert.ChangeType(data.ToString(), typeof(T))); return; } @@ -404,11 +395,9 @@ namespace CryptoExchange.Net } dataHandler(desResult.Data); - }; + } - if (request != null) - return connection.AddHandler(request, userSubscription, internalHandler); - return connection.AddHandler(identifier, userSubscription, internalHandler); + return connection.AddHandler(request ?? identifier, userSubscription, InternalHandler); } /// @@ -515,16 +504,16 @@ namespace CryptoExchange.Net break; var obj = objGetter(socket); - if (obj != null) + if (obj == null) + continue; + + try { - try - { - socket.Send(obj); - } - catch (Exception ex) - { - log.Write(LogVerbosity.Warning, "Periodic send failed: " + ex); - } + socket.Send(obj); + } + catch (Exception ex) + { + log.Write(LogVerbosity.Warning, "Periodic send failed: " + ex); } } } diff --git a/CryptoExchange.Net/Sockets/BaseSocket.cs b/CryptoExchange.Net/Sockets/BaseSocket.cs index fee144a..0e8d5c6 100644 --- a/CryptoExchange.Net/Sockets/BaseSocket.cs +++ b/CryptoExchange.Net/Sockets/BaseSocket.cs @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Sockets /// /// Socket implementation /// - public class BaseSocket: IWebsocket + internal class BaseSocket: IWebsocket { internal static int lastStreamId; private static readonly object streamIdLock = new object(); @@ -153,7 +153,7 @@ namespace CryptoExchange.Net.Sockets if (DateTime.UtcNow - LastActionTime > Timeout) { log.Write(LogVerbosity.Warning, $"No data received for {Timeout}, reconnecting socket"); - Close().ConfigureAwait(false); + _ = Close().ConfigureAwait(false); return; } } @@ -267,7 +267,7 @@ namespace CryptoExchange.Net.Sockets if (connected) { log?.Write(LogVerbosity.Debug, $"Socket {Id} connected"); - if ((timeoutTask == null || timeoutTask.IsCompleted) && Timeout != default(TimeSpan)) + if ((timeoutTask == null || timeoutTask.IsCompleted) && Timeout != default) timeoutTask = Task.Run(CheckTimeout); } else diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 2bb0df3..a13585e 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -172,7 +172,7 @@ namespace CryptoExchange.Net.Sockets SocketSubscription currentSubscription = null; try { - bool handled = false; + var handled = false; var sw = Stopwatch.StartNew(); lock (handlersLock) { @@ -387,7 +387,7 @@ namespace CryptoExchange.Net.Sockets if (subscription.Confirmed) await socketClient.Unsubscribe(this, subscription).ConfigureAwait(false); - bool shouldCloseWrapper = false; + var shouldCloseWrapper = false; lock (handlersLock) { handlers.Remove(subscription); From 546539fe8f98aaa8846069a605b04bb1b1155890 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Thu, 5 Sep 2019 11:04:28 +0200 Subject: [PATCH 09/41] Set language version for travis --- CryptoExchange.Net/CryptoExchange.Net.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index a7744fa..44fa670 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -12,6 +12,7 @@ en true 2.1.8 - Added array serialization options for implementations + 7.1 CryptoExchange.Net.xml From 715fe378d5d10db64b8d0a717158e456c63d4eb3 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Tue, 10 Sep 2019 14:15:53 +0200 Subject: [PATCH 10/41] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 988e4ce..efb84c8 100644 --- a/README.md +++ b/README.md @@ -130,10 +130,10 @@ Note that when using a file it can provide credentials for multiple exchanges by ```` // File content: { - "binanceKey": "binanceApiKey", - "binanceSecret": "binanceApiSecret", - "bittrexKey": "bitrexApiKey", - "bittrexSecret": "bittrexApiSecret", + "binanceKey": "actualBinanceApiKey", + "binanceSecret": "actualBinanceApiSecret", + "bittrexKey": "actualBittrexApiKey", + "bittrexSecret": "actualBittrexApiSecret", } // Loading: @@ -145,7 +145,7 @@ using (var stream = File.OpenRead("/path/to/credential-file")) }); BittrexClient.SetDefaultOptions(new BittrexClientOptions { - ApiCredentials = new ApiCredentials(stream, "BittrexKey", "BittrexSecret") + ApiCredentials = new ApiCredentials(stream, "bittrexKey", "bittrexSecret") }); } ```` From 8ec902951d4e442299bfdc5e401d45e66c6e0f1f Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Fri, 11 Oct 2019 14:48:30 +0200 Subject: [PATCH 11/41] 3.0 wip --- .../Authentication/ApiCredentials.cs | 67 ++-- .../Authentication/AuthenticationProvider.cs | 5 +- .../Authentication/PrivateKey.cs | 20 +- CryptoExchange.Net/BaseClient.cs | 94 +++-- .../Converters/ArrayConverter.cs | 8 +- .../Converters/BaseConverter.cs | 7 +- .../Converters/TimestampConverter.cs | 2 +- .../TimestampNanoSecondsConverter.cs | 2 +- .../Converters/TimestampSecondsConverter.cs | 5 +- .../Converters/UTCDateTimeConverter.cs | 2 +- CryptoExchange.Net/CryptoExchange.Net.csproj | 7 +- CryptoExchange.Net/CryptoExchange.Net.xml | 329 +++++++++--------- CryptoExchange.Net/ExtensionMethods.cs | 27 +- CryptoExchange.Net/Interfaces/IRequest.cs | 74 ++-- .../Interfaces/IRequestFactory.cs | 16 +- CryptoExchange.Net/Interfaces/IResponse.cs | 23 +- CryptoExchange.Net/Interfaces/IWebsocket.cs | 14 +- .../Logging/ThreadSafeFileWriter.cs | 7 +- CryptoExchange.Net/Objects/ApiProxy.cs | 35 +- CryptoExchange.Net/Objects/CallResult.cs | 23 +- CryptoExchange.Net/Objects/Constants.cs | 17 - CryptoExchange.Net/Objects/Error.cs | 11 + CryptoExchange.Net/Objects/Options.cs | 73 +++- .../OrderBook/SymbolOrderBook.cs | 21 +- .../RateLimiter/RateLimiterAPIKey.cs | 2 +- CryptoExchange.Net/Requests/Request.cs | 63 ++-- CryptoExchange.Net/Requests/RequestFactory.cs | 30 +- CryptoExchange.Net/Requests/Response.cs | 33 +- CryptoExchange.Net/RestClient.cs | 230 +++++------- CryptoExchange.Net/SocketClient.cs | 53 ++- CryptoExchange.Net/Sockets/BaseSocket.cs | 34 +- .../Sockets/SocketConnection.cs | 22 +- .../Sockets/SocketSubscription.cs | 24 +- 33 files changed, 725 insertions(+), 655 deletions(-) diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index e6f1dcb..f863fcd 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -14,17 +14,17 @@ namespace CryptoExchange.Net.Authentication /// /// The api key to authenticate requests /// - public SecureString Key { get; } + public SecureString? Key { get; } /// /// The api secret to authenticate requests /// - public SecureString Secret { get; } + public SecureString? Secret { get; } /// /// The private key to authenticate requests /// - public PrivateKey PrivateKey { get; } + public PrivateKey? PrivateKey { get; } /// /// Create Api credentials providing a private key for authentication @@ -56,8 +56,20 @@ namespace CryptoExchange.Net.Authentication if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) throw new ArgumentException("Key and secret can't be null/empty"); - Key = CreateSecureString(key); - Secret = CreateSecureString(secret); + Key = key.ToSecureString(); + Secret = secret.ToSecureString(); + } + + /// + /// Copy the credentials + /// + /// + public ApiCredentials Copy() + { + if (PrivateKey == null) + return new ApiCredentials(Key!.GetString(), Secret!.GetString()); + else + return new ApiCredentials(PrivateKey!.Copy()); } /// @@ -66,24 +78,23 @@ namespace CryptoExchange.Net.Authentication /// The stream containing the json data /// A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'. /// A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'. - public ApiCredentials(Stream inputStream, string identifierKey = null, string identifierSecret = null) + public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) { - using (var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true)) - { - var stringData = reader.ReadToEnd(); - var jsonData = stringData.ToJToken(); - if(jsonData == null) - throw new ArgumentException("Input stream not valid json data"); + using var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true); + + var stringData = reader.ReadToEnd(); + var jsonData = stringData.ToJToken(); + if(jsonData == null) + throw new ArgumentException("Input stream not valid json data"); - var key = TryGetValue(jsonData, identifierKey ?? "apiKey"); - var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret"); + var key = TryGetValue(jsonData, identifierKey ?? "apiKey"); + var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret"); - if (key == null || secret == null) - throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); + if (key == null || secret == null) + throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); - Key = CreateSecureString(key); - Secret = CreateSecureString(secret); - } + Key = key.ToSecureString(); + Secret = secret.ToSecureString(); inputStream.Seek(0, SeekOrigin.Begin); } @@ -94,26 +105,12 @@ namespace CryptoExchange.Net.Authentication /// /// /// - protected string TryGetValue(JToken data, string key) + protected string? TryGetValue(JToken data, string key) { if (data[key] == null) return null; return (string) data[key]; - } - - /// - /// Create a secure string from a string - /// - /// - /// - protected SecureString CreateSecureString(string source) - { - var secureString = new SecureString(); - foreach (var c in source) - secureString.AppendChar(c); - secureString.MakeReadOnly(); - return secureString; - } + } /// /// Dispose diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index d2a41a0..8739fed 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; namespace CryptoExchange.Net.Authentication { @@ -29,7 +30,7 @@ namespace CryptoExchange.Net.Authentication /// /// /// - public virtual Dictionary AddAuthenticationToParameters(string uri, string method, Dictionary parameters, bool signed) + public virtual Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed) { return parameters; } @@ -42,7 +43,7 @@ namespace CryptoExchange.Net.Authentication /// /// /// - public virtual Dictionary AddAuthenticationToHeaders(string uri, string method, Dictionary parameters, bool signed) + public virtual Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed) { return new Dictionary(); } diff --git a/CryptoExchange.Net/Authentication/PrivateKey.cs b/CryptoExchange.Net/Authentication/PrivateKey.cs index 47c4755..238e9b8 100644 --- a/CryptoExchange.Net/Authentication/PrivateKey.cs +++ b/CryptoExchange.Net/Authentication/PrivateKey.cs @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Authentication /// /// The private key's pass phrase /// - public SecureString Passphrase { get; } + public SecureString? Passphrase { get; } /// /// Indicates if the private key is encrypted or not @@ -81,15 +81,23 @@ namespace CryptoExchange.Net.Authentication if (string.IsNullOrEmpty(key)) throw new ArgumentException("Key can't be null/empty"); - var secureKey = new SecureString(); - foreach (var c in key) - secureKey.AppendChar(c); - secureKey.MakeReadOnly(); - Key = secureKey; + Key = key.ToSecureString(); IsEncrypted = false; } + /// + /// Copy the private key + /// + /// + public PrivateKey Copy() + { + if (Passphrase == null) + return new PrivateKey(Key.GetString()); + else + return new PrivateKey(Key.GetString(), Passphrase.GetString()); + } + /// /// Dispose /// diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index b4e79ed..699f349 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -7,15 +7,17 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; namespace CryptoExchange.Net { /// /// The base for all clients /// - public abstract class BaseClient: IDisposable + public abstract class BaseClient : IDisposable { /// /// The address of the client @@ -28,11 +30,11 @@ namespace CryptoExchange.Net /// /// The api proxy /// - protected ApiProxy apiProxy; + protected ApiProxy? apiProxy; /// /// The auth provider /// - protected internal AuthenticationProvider authProvider; + protected internal AuthenticationProvider? authProvider; /// /// The last used id @@ -59,26 +61,17 @@ namespace CryptoExchange.Net /// /// /// - protected BaseClient(ClientOptions options, AuthenticationProvider authenticationProvider) + protected BaseClient(ClientOptions options, AuthenticationProvider? authenticationProvider) { log = new Log(); authProvider = authenticationProvider; - Configure(options); - } + log.UpdateWriters(options.LogWriters); + log.Level = options.LogVerbosity; - /// - /// Configure the client using the provided options - /// - /// Options - protected void Configure(ClientOptions clientOptions) - { - log.UpdateWriters(clientOptions.LogWriters); - log.Level = clientOptions.LogVerbosity; + BaseAddress = options.BaseAddress; + apiProxy = options.Proxy; - BaseAddress = clientOptions.BaseAddress; - apiProxy = clientOptions.Proxy; - if (apiProxy != null) - log.Write(LogVerbosity.Info, $"Setting api proxy to {clientOptions.Proxy.Host}:{clientOptions.Proxy.Port}"); + log.Write(LogVerbosity.Debug, $"Client configuration: {options}"); } /// @@ -137,10 +130,10 @@ namespace CryptoExchange.Net /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use /// - protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer serializer = null) + protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer? serializer = null) { var tokenResult = ValidateJson(data); - return !tokenResult.Success ? new CallResult(default, tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer); + return !tokenResult ? new CallResult(default, tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer); } /// @@ -151,7 +144,7 @@ namespace CryptoExchange.Net /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use /// - protected CallResult Deserialize(JToken obj, bool checkObject = true, JsonSerializer serializer = null) + protected CallResult Deserialize(JToken obj, bool checkObject = true, JsonSerializer? serializer = null) { if (serializer == null) serializer = defaultSerializer; @@ -200,6 +193,53 @@ namespace CryptoExchange.Net } } + /// + /// Deserialize a stream into an object + /// + /// The type to deserialize into + /// The stream to deserialize + /// A specific serializer to use + /// + protected async Task> Deserialize(Stream stream, JsonSerializer? serializer = null) + { + if (serializer == null) + serializer = defaultSerializer; + + try + { + using var reader = new StreamReader(stream); + using var jsonReader = new JsonTextReader(reader); + return new CallResult(serializer.Deserialize(jsonReader), null); + } + catch (JsonReaderException jre) + { + var data = await ReadStream(stream).ConfigureAwait(false); + var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"; + log.Write(LogVerbosity.Error, info); + return new CallResult(default, new DeserializeError(info)); + } + catch (JsonSerializationException jse) + { + var data = await ReadStream(stream).ConfigureAwait(false); + var info = $"Deserialize JsonSerializationException: {jse.Message}, data: {data}"; + log.Write(LogVerbosity.Error, info); + return new CallResult(default, new DeserializeError(info)); + } + catch (Exception ex) + { + var data = await ReadStream(stream).ConfigureAwait(false); + var info = $"Deserialize Unknown Exception: {ex.Message}, data: {data}"; + log.Write(LogVerbosity.Error, info); + return new CallResult(default, new DeserializeError(info)); + } + } + + private async Task ReadStream(Stream stream) + { + using var reader = new StreamReader(stream); + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + private void CheckObject(Type type, JObject obj) { if (type.GetCustomAttribute(true) != null) @@ -233,13 +273,17 @@ namespace CryptoExchange.Net if (d == null) { d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase)); - if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) + if (d == null) { - log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); - isDif = true; + if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) + { + log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); + isDif = true; + } continue; } } + properties.Remove(d); var propType = GetProperty(d, props)?.PropertyType; @@ -270,7 +314,7 @@ namespace CryptoExchange.Net log.Write(LogVerbosity.Debug, "Returned data: " + obj); } - private static PropertyInfo GetProperty(string name, IEnumerable props) + private static PropertyInfo? GetProperty(string name, IEnumerable props) { foreach (var prop in props) { diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs index ce6dede..4d7b8bc 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/ArrayConverter.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (objectType == typeof(JToken)) return JToken.Load(reader); @@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Converters return ParseObject(arr, result, objectType); } - private static object ParseObject(JArray arr, object result, Type objectType) + private static object? ParseObject(JArray arr, object result, Type objectType) { foreach (var property in objectType.GetProperties()) { @@ -76,7 +76,7 @@ namespace CryptoExchange.Net.Converters var converterAttribute = (JsonConverterAttribute)property.GetCustomAttribute(typeof(JsonConverterAttribute)) ?? (JsonConverterAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConverterAttribute)); var conversionAttribute = (JsonConversionAttribute)property.GetCustomAttribute(typeof(JsonConversionAttribute)) ?? (JsonConversionAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConversionAttribute)); - object value; + object? value; if (converterAttribute != null) { value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); @@ -133,7 +133,7 @@ namespace CryptoExchange.Net.Converters while (arrayProp.Index != last + 1) { - writer.WriteValue((string)null); + writer.WriteValue((string?)null); last += 1; } diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index b458149..97f18d0 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Newtonsoft.Json; @@ -10,7 +11,7 @@ namespace CryptoExchange.Net.Converters /// Base class for enum converters /// /// Type of enum to convert - public abstract class BaseConverter: JsonConverter + public abstract class BaseConverter: JsonConverter where T: struct { /// /// The enum->string mapping @@ -38,7 +39,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; @@ -69,7 +70,7 @@ namespace CryptoExchange.Net.Converters return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T); } - private bool GetValue(string value, out T result) + private bool GetValue(string value, [NotNullWhen(false)]out T result) { var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); if (!mapping.Equals(default(KeyValuePair))) diff --git a/CryptoExchange.Net/Converters/TimestampConverter.cs b/CryptoExchange.Net/Converters/TimestampConverter.cs index 6d08a0d..f1b9103 100644 --- a/CryptoExchange.Net/Converters/TimestampConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampConverter.cs @@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; diff --git a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs index fb8205d..80727b6 100644 --- a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; diff --git a/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs index c1dff7e..1b2264a 100644 --- a/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs @@ -16,8 +16,11 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { + if (reader.Value == null) + return null; + if (reader.Value is double d) return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d); diff --git a/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs b/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs index 5210ccf..9690f7f 100644 --- a/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs @@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 44fa670..783ef3e 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,7 +1,7 @@ - netstandard2.0 - + netstandard2.1 + CryptoExchange.Net JKorf @@ -12,7 +12,8 @@ en true 2.1.8 - Added array serialization options for implementations - 7.1 + enable + 8.0 CryptoExchange.Net.xml diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 482e013..0a8433e 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -54,6 +54,12 @@ The api key used for identification The api secret used for signing + + + Copy the credentials + + + Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret @@ -70,13 +76,6 @@ - - - Create a secure string from a string - - - - Dispose @@ -98,7 +97,7 @@ - + Add authentication to the parameter list @@ -108,7 +107,7 @@ - + Add authentication to the header dictionary @@ -185,6 +184,12 @@ The private key used for signing + + + Copy the private key + + + Dispose @@ -237,12 +242,6 @@ - - - Configure the client using the provided options - - Options - Set the authentication provider @@ -276,6 +275,15 @@ A specific serializer to use + + + Deserialize a stream into an object + + The type to deserialize into + The stream to deserialize + A specific serializer to use + + Generate a unique id @@ -467,11 +475,11 @@ The source secure string - + - Header collection to IEnumerable + Create a secure string from a string - + @@ -518,65 +526,49 @@ Request interface - + - The uri of the request - - - - - The headers of the request - - - - - The method of the request - - - - - The timeout of the request - - - - - Set a proxy - - - - - - - - - Content type + Accept header - String content + Content - + - Accept + Headers - + - Content length + Method - + - Get the request stream + Uri - - + - Get the response object + Set byte content + + + + + Set string content + + + + + + + Get the response + + @@ -584,13 +576,21 @@ Request factory interface - + Create a request for an uri + + + + Configure the requests created by this factory + + Request timeout to use + Proxy settings to use + Response object interface @@ -601,18 +601,22 @@ The response status code + + + Whether the status code indicates a success status + + + + + The response headers + + Get the response stream - - - Get the response headers - - - Close the response @@ -883,16 +887,6 @@ Is open - - - Should ping connecting - - - - - Interval of pinging - - Supported ssl protocols @@ -1090,6 +1084,16 @@ The proxy login The proxy password + + + + Create new settings for a proxy + + The proxy hostname/ip + The proxy port + The proxy login + The proxy password + Comparer for byte order @@ -1147,7 +1151,7 @@ The response headers - + ctor @@ -1163,7 +1167,7 @@ - + Create an error result @@ -1177,26 +1181,6 @@ Constants - - - GET Http method - - - - - POST Http method - - - - - DELETE Http method - - - - - PUT Http method - - Json content type header @@ -1428,6 +1412,16 @@ + + + Cancellation requested + + + + + ctor + + Base options @@ -1443,6 +1437,9 @@ The log writers + + + Base for order book options @@ -1464,26 +1461,38 @@ The name of the order book implementation Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. + + + Base client options - - - The api credentials - - The base address of the client + + + The api credentials + + Proxy to use + + + ctor + + + + + + Base for rest client options @@ -1504,6 +1513,12 @@ The time the server has to respond to a request before timing out + + + ctor + + + Create a copy of the options @@ -1511,6 +1526,9 @@ + + + Base for socket client options @@ -1542,6 +1560,12 @@ Setting this to a higher number increases subscription speed, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection. + + + ctor + + + Create a copy of the options @@ -1549,6 +1573,9 @@ + + + Order book entry @@ -1876,43 +1903,35 @@ Request object - + Create request object for web request + - - - - - - - - - - + - + - + @@ -1920,7 +1939,10 @@ WebRequest factory - + + + + @@ -1931,16 +1953,19 @@ - - - Create response for http web response - - - - + - + + + + + + Create response for a http response message + + The actual response + + @@ -1998,12 +2023,6 @@ - - - Configure the client using the provided options - - Options - Adds a rate limiter to the client. There are 2 choices, the and the . @@ -2027,26 +2046,28 @@ The roundtrip time of the ping request - + Execute a request The expected result type The uri to send the request to The method of the request + Cancellation token The parameters of the request Whether or not the request should be authenticated Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) - + - Can be overridden to indicate if a response is an error response + Executes the request and returns the string result - The received data - True if error response + The request object to execute + Cancellation token + - + Creates a request object @@ -2056,26 +2077,13 @@ Whether or not the request should be authenticated - - - Writes the string data of the parameters to the request body stream - - - - - + Writes the parameters of the request to the request object, either in the query string or the request body - - - - Executes the request and returns the string result - - The request object to execute - + @@ -2166,12 +2174,6 @@ Client options Authentication provider - - - Configure the client using the provided options - - Options - Set a function to interpret the data, used when the data is received as bytes instead of a string @@ -2536,12 +2538,19 @@ If the subscription has been confirmed - + + + ctor + + + + + + ctor - diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 8d2d2c2..b8133b4 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -47,7 +47,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddOptionalParameter(this Dictionary parameters, string key, object value) + public static void AddOptionalParameter(this Dictionary parameters, string key, object? value) { if(value != null) parameters.Add(key, value); @@ -59,7 +59,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddOptionalParameter(this Dictionary parameters, string key, string value) + public static void AddOptionalParameter(this Dictionary parameters, string key, string? value) { if (value != null) parameters.Add(key, value); @@ -127,20 +127,17 @@ namespace CryptoExchange.Net } /// - /// Header collection to IEnumerable + /// Create a secure string from a string /// - /// + /// /// - public static IEnumerable> ToIEnumerable(this WebHeaderCollection headers) + internal static SecureString ToSecureString(this string source) { - if (headers == null) - return null; - - return Enumerable - .Range(0, headers.Count) - .SelectMany(i => headers.GetValues(i) - .Select(v => Tuple.Create(headers.GetKey(i), v)) - ); + var secureString = new SecureString(); + foreach (var c in source) + secureString.AppendChar(c); + secureString.MakeReadOnly(); + return secureString; } /// @@ -152,7 +149,7 @@ namespace CryptoExchange.Net /// public static async Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken) { - RegisteredWaitHandle registeredHandle = null; + RegisteredWaitHandle? registeredHandle = null; CancellationTokenRegistration tokenRegistration = default; try { @@ -192,7 +189,7 @@ namespace CryptoExchange.Net /// /// /// - public static JToken ToJToken(this string stringData, Log log = null) + public static JToken? ToJToken(this string stringData, Log? log = null) { if (string.IsNullOrEmpty(stringData)) return null; diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 6c42ca5..656a276 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,6 +1,7 @@ using System; -using System.IO; -using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces @@ -11,56 +12,41 @@ namespace CryptoExchange.Net.Interfaces public interface IRequest { /// - /// The uri of the request + /// Accept header + /// + string Accept { set; } + /// + /// Content + /// + string? Content { get; } + /// + /// Headers + /// + HttpRequestHeaders Headers { get; } + /// + /// Method + /// + HttpMethod Method { get; set; } + /// + /// Uri /// Uri Uri { get; } /// - /// The headers of the request + /// Set byte content /// - WebHeaderCollection Headers { get; set; } + /// + void SetContent(byte[] data); /// - /// The method of the request + /// Set string content /// - string Method { get; set; } + /// + /// + void SetContent(string data, string contentType); /// - /// The timeout of the request - /// - TimeSpan Timeout { get; set; } - /// - /// Set a proxy - /// - /// - /// - /// - /// - void SetProxy(string host, int port, string login, string password); - - /// - /// Content type - /// - string ContentType { get; set; } - /// - /// String content - /// - string Content { get; set; } - /// - /// Accept - /// - string Accept { get; set; } - /// - /// Content length - /// - long ContentLength { get; set; } - - /// - /// Get the request stream + /// Get the response /// + /// /// - Task GetRequestStream(); - /// - /// Get the response object - /// - /// - Task GetResponse(); + Task GetResponse(CancellationToken cancellationToken); } } diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index 193db1d..ceb7f8c 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -1,4 +1,8 @@ -namespace CryptoExchange.Net.Interfaces +using CryptoExchange.Net.Objects; +using System; +using System.Net.Http; + +namespace CryptoExchange.Net.Interfaces { /// /// Request factory interface @@ -8,8 +12,16 @@ /// /// Create a request for an uri /// + /// /// /// - IRequest Create(string uri); + IRequest Create(HttpMethod method, string uri); + + /// + /// Configure the requests created by this factory + /// + /// Request timeout to use + /// Proxy settings to use + void Configure(TimeSpan requestTimeout, ApiProxy? proxy); } } diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index cc32604..9ebb888 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces { @@ -14,16 +14,23 @@ namespace CryptoExchange.Net.Interfaces /// The response status code /// HttpStatusCode StatusCode { get; } + + /// + /// Whether the status code indicates a success status + /// + bool IsSuccessStatusCode { get; } + + /// + /// The response headers + /// + IEnumerable>> ResponseHeaders { get; } + /// /// Get the response stream /// /// - Stream GetResponseStream(); - /// - /// Get the response headers - /// - /// - IEnumerable> GetResponseHeaders(); + Task GetResponseStream(); + /// /// Close the response /// diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index 6a97da1..ede336e 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Origin /// - string Origin { get; set; } + string? Origin { get; set; } /// /// Reconnecting /// @@ -42,11 +42,11 @@ namespace CryptoExchange.Net.Interfaces /// /// Handler for byte data /// - Func DataInterpreterBytes { get; set; } + Func? DataInterpreterBytes { get; set; } /// /// Handler for string data /// - Func DataInterpreterString { get; set; } + Func? DataInterpreterString { get; set; } /// /// Socket url /// @@ -64,14 +64,6 @@ namespace CryptoExchange.Net.Interfaces /// bool IsOpen { get; } /// - /// Should ping connecting - /// - bool PingConnection { get; set; } - /// - /// Interval of pinging - /// - TimeSpan PingInterval { get; set; } - /// /// Supported ssl protocols /// SslProtocols SSLProtocols { get; set; } diff --git a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs index 5caac53..97c6ce0 100644 --- a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs +++ b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs @@ -49,11 +49,8 @@ namespace CryptoExchange.Net.Logging /// protected override void Dispose(bool disposing) { - lock (writeLock) - { - logWriter.Close(); - logWriter = null; - } + lock (writeLock) + logWriter.Close(); } } } diff --git a/CryptoExchange.Net/Objects/ApiProxy.cs b/CryptoExchange.Net/Objects/ApiProxy.cs index b4db095..9b66e7f 100644 --- a/CryptoExchange.Net/Objects/ApiProxy.cs +++ b/CryptoExchange.Net/Objects/ApiProxy.cs @@ -1,4 +1,5 @@ using System; +using System.Security; namespace CryptoExchange.Net.Objects { @@ -19,25 +20,20 @@ namespace CryptoExchange.Net.Objects /// /// The login of the proxy /// - public string Login { get; } + public string? Login { get; } /// /// The password of the proxy /// - public string Password { get; } + public SecureString? Password { get; } /// /// Create new settings for a proxy /// /// The proxy hostname/ip /// The proxy port - public ApiProxy(string host, int port) + public ApiProxy(string host, int port): this(host, port, null, (SecureString?)null) { - if(string.IsNullOrEmpty(host) || port <= 0) - throw new ArgumentException("Proxy host or port not filled"); - - Host = host; - Port = port; } /// @@ -48,11 +44,28 @@ namespace CryptoExchange.Net.Objects /// The proxy port /// The proxy login /// The proxy password - public ApiProxy(string host, int port, string login, string password) : this(host, port) + public ApiProxy(string host, int port, string? login, string? password) : this(host, port, login, password?.ToSecureString()) { - if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) - throw new ArgumentException("Proxy login or password not filled"); + } + /// + /// + /// Create new settings for a proxy + /// + /// The proxy hostname/ip + /// The proxy port + /// The proxy login + /// The proxy password + public ApiProxy(string host, int port, string? login, SecureString? password) + { + if (string.IsNullOrEmpty(login)) + throw new ArgumentException("Proxy login not provided"); + + if (!host.StartsWith("http")) + throw new ArgumentException("Proxy host should start with either http:// or https://"); + + Host = host; + Port = port; Login = login; Password = password; } diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 1085328..78044f4 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; namespace CryptoExchange.Net.Objects @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects /// /// An error if the call didn't succeed /// - public Error Error { get; internal set; } + public Error? Error { get; internal set; } /// /// Whether the call was successful /// @@ -28,11 +28,20 @@ namespace CryptoExchange.Net.Objects /// /// /// - public CallResult(T data, Error error) + public CallResult([AllowNull]T data, Error? error) { Data = data; Error = error; } + + /// + /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) + /// + /// + public static implicit operator bool(CallResult obj) + { + return !ReferenceEquals(obj, null) && obj.Success; + } } /// @@ -49,7 +58,7 @@ namespace CryptoExchange.Net.Objects /// /// The response headers /// - public IEnumerable> ResponseHeaders { get; set; } + public IEnumerable>>? ResponseHeaders { get; set; } /// /// ctor @@ -58,7 +67,7 @@ namespace CryptoExchange.Net.Objects /// /// /// - public WebCallResult(HttpStatusCode? code, IEnumerable> responseHeaders, T data, Error error): base(data, error) + public WebCallResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) { ResponseHeaders = responseHeaders; ResponseStatusCode = code; @@ -81,7 +90,7 @@ namespace CryptoExchange.Net.Objects /// /// /// - public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable> responseHeaders, Error error) + public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>> responseHeaders, Error error) { return new WebCallResult(code, responseHeaders, default, error); } diff --git a/CryptoExchange.Net/Objects/Constants.cs b/CryptoExchange.Net/Objects/Constants.cs index 55112fa..82e5af5 100644 --- a/CryptoExchange.Net/Objects/Constants.cs +++ b/CryptoExchange.Net/Objects/Constants.cs @@ -5,23 +5,6 @@ /// public class Constants { - /// - /// GET Http method - /// - public const string GetMethod = "GET"; - /// - /// POST Http method - /// - public const string PostMethod = "POST"; - /// - /// DELETE Http method - /// - public const string DeleteMethod = "DELETE"; - /// - /// PUT Http method - /// - public const string PutMethod = "PUT"; - /// /// Json content type header /// diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 29240b1..0836d3e 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -137,4 +137,15 @@ /// public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { } } + + /// + /// Cancellation requested + /// + public class CancellationRequestedError : Error + { + /// + /// ctor + /// + public CancellationRequestedError() : base(9, "Cancellation requested") { } + } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 22c3729..e7e67c7 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -21,6 +21,12 @@ namespace CryptoExchange.Net.Objects /// The log writers /// public List LogWriters { get; set; } = new List { new DebugTextWriter() }; + + /// + public override string ToString() + { + return $"LogVerbosity: {LogVerbosity}, Writers: {LogWriters.Count}"; + } } /// @@ -47,6 +53,12 @@ namespace CryptoExchange.Net.Objects OrderBookName = name; SequenceNumbersAreConsecutive = sequencesAreConsecutive; } + + /// + public override string ToString() + { + return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}"; + } } /// @@ -54,21 +66,36 @@ namespace CryptoExchange.Net.Objects /// public class ClientOptions : BaseOptions { - - /// - /// The api credentials - /// - public ApiCredentials ApiCredentials { get; set; } - /// /// The base address of the client /// public string BaseAddress { get; set; } + /// + /// The api credentials + /// + public ApiCredentials? ApiCredentials { get; set; } + + /// /// Proxy to use /// - public ApiProxy Proxy { get; set; } + public ApiProxy? Proxy { get; set; } + + /// + /// ctor + /// + /// + public ClientOptions(string baseAddress) + { + BaseAddress = baseAddress; + } + + /// + public override string ToString() + { + return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-": "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null? "-": Proxy.Host)}"; + } } /// @@ -91,6 +118,14 @@ namespace CryptoExchange.Net.Objects /// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// + /// ctor + /// + /// + public RestClientOptions(string baseAddress): base(baseAddress) + { + } + /// /// Create a copy of the options /// @@ -110,10 +145,16 @@ namespace CryptoExchange.Net.Objects }; if (ApiCredentials != null) - copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString()); + copy.ApiCredentials = ApiCredentials.Copy(); return copy; } + + /// + public override string ToString() + { + return $"{base.ToString()}, RateLimitters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout.ToString("c")}"; + } } /// @@ -146,6 +187,14 @@ namespace CryptoExchange.Net.Objects /// public int? SocketSubscriptionsCombineTarget { get; set; } + /// + /// ctor + /// + /// + public SocketClientOptions(string baseAddress) : base(baseAddress) + { + } + /// /// Create a copy of the options /// @@ -166,9 +215,15 @@ namespace CryptoExchange.Net.Objects }; if (ApiCredentials != null) - copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString()); + copy.ApiCredentials = ApiCredentials.Copy(); return copy; } + + /// + public override string ToString() + { + return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout.ToString("c")}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; + } } } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 47671b2..65aab30 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -31,7 +31,7 @@ namespace CryptoExchange.Net.OrderBook protected SortedList bids; private OrderBookStatus status; - private UpdateSubscription subscription; + private UpdateSubscription? subscription; private readonly bool sequencesAreConsecutive; private readonly string id; /// @@ -71,11 +71,11 @@ namespace CryptoExchange.Net.OrderBook /// /// Event when the state changes /// - public event Action OnStatusChange; + public event Action? OnStatusChange; /// /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets /// - public event Action OnOrderBookUpdate; + public event Action? OnOrderBookUpdate; /// /// Timestamp of the last update /// @@ -145,6 +145,12 @@ namespace CryptoExchange.Net.OrderBook /// protected SymbolOrderBook(string symbol, OrderBookOptions options) { + if (symbol == null) + throw new ArgumentNullException("symbol"); + + if (options == null) + throw new ArgumentNullException("options"); + id = options.OrderBookName; processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; @@ -173,7 +179,7 @@ namespace CryptoExchange.Net.OrderBook { Status = OrderBookStatus.Connecting; var startResult = await DoStart().ConfigureAwait(false); - if (!startResult.Success) + if (!startResult) return new CallResult(false, startResult.Error); subscription = startResult.Data; @@ -202,7 +208,7 @@ namespace CryptoExchange.Net.OrderBook return; var resyncResult = DoResync().Result; - success = resyncResult.Success; + success = resyncResult; } log.Write(LogVerbosity.Info, $"{id} order book {Symbol} successfully resynchronized"); @@ -222,7 +228,8 @@ namespace CryptoExchange.Net.OrderBook public async Task StopAsync() { Status = OrderBookStatus.Disconnected; - await subscription.Close().ConfigureAwait(false); + if(subscription != null) + await subscription.Close().ConfigureAwait(false); } /// @@ -303,7 +310,7 @@ namespace CryptoExchange.Net.OrderBook { // Out of sync log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} out of sync, reconnecting"); - subscription.Reconnect().Wait(); + subscription!.Reconnect().Wait(); } else { diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs index 2e8ea4f..4bd9af3 100644 --- a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs +++ b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs @@ -32,7 +32,7 @@ namespace CryptoExchange.Net.RateLimiter /// public CallResult LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour) { - if(client.authProvider?.Credentials == null) + if(client.authProvider?.Credentials?.Key == null) return new CallResult(0, null); var key = client.authProvider.Credentials.Key.GetString(); diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 611f38c..4edbd5f 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,6 +1,8 @@ using System; -using System.IO; -using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; @@ -9,84 +11,61 @@ namespace CryptoExchange.Net.Requests /// /// Request object /// - public class Request : IRequest + internal class Request : IRequest { - private readonly WebRequest request; + private readonly HttpRequestMessage request; + private readonly HttpClient httpClient; /// /// Create request object for web request /// /// - public Request(WebRequest request) + /// + public Request(HttpRequestMessage request, HttpClient client) { + httpClient = client; this.request = request; } /// - public WebHeaderCollection Headers - { - get => request.Headers; - set => request.Headers = value; - } + public HttpRequestHeaders Headers => request.Headers; /// - public string ContentType - { - get => request.ContentType; - set => request.ContentType = value; - } - - /// - public string Content { get; set; } + public string? Content { get; private set; } /// public string Accept { - get => ((HttpWebRequest)request).Accept; - set => ((HttpWebRequest)request).Accept = value; + set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value)); } /// - public long ContentLength - { - get => ((HttpWebRequest)request).ContentLength; - set => ((HttpWebRequest)request).ContentLength = value; - } - - /// - public string Method + public HttpMethod Method { get => request.Method; set => request.Method = value; } - /// - public TimeSpan Timeout - { - get => TimeSpan.FromMilliseconds(request.Timeout); - set => request.Timeout = (int)Math.Round(value.TotalMilliseconds); - } - /// public Uri Uri => request.RequestUri; /// - public void SetProxy(string host, int port, string login, string password) + public void SetContent(string data, string contentType) { - request.Proxy = new WebProxy(host, port); - if(!string.IsNullOrEmpty(login) && !string.IsNullOrEmpty(password)) request.Proxy.Credentials = new NetworkCredential(login, password); + Content = data; + request.Content = new StringContent(data, Encoding.UTF8, contentType); } /// - public async Task GetRequestStream() + public void SetContent(byte[] data) { - return await request.GetRequestStreamAsync().ConfigureAwait(false); + request.Content = new ByteArrayContent(data); } /// - public async Task GetResponse() + public async Task GetResponse(CancellationToken cancellationToken) { - return new Response((HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false)); + return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); } } } diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index add3f81..b91e188 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -1,5 +1,8 @@ -using System.Net; +using System; +using System.Net; +using System.Net.Http; using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.Requests { @@ -8,10 +11,31 @@ namespace CryptoExchange.Net.Requests /// public class RequestFactory : IRequestFactory { + private HttpClient? httpClient; + /// - public IRequest Create(string uri) + public void Configure(TimeSpan requestTimeout, ApiProxy? proxy) { - return new Request(WebRequest.Create(uri)); + HttpMessageHandler handler = new HttpClientHandler() + { + Proxy = proxy == null ? null : new WebProxy + { + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) + } + }; + + httpClient = new HttpClient(handler); + httpClient.Timeout = requestTimeout; + } + + /// + public IRequest Create(HttpMethod method, string uri) + { + if (httpClient == null) + throw new InvalidOperationException("Cant create request before configuring http client"); + + return new Request(new HttpRequestMessage(method, uri), httpClient); } } } diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index a5d88a1..3df463d 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -1,7 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; namespace CryptoExchange.Net.Requests @@ -9,38 +10,38 @@ namespace CryptoExchange.Net.Requests /// /// HttpWebResponse response object /// - public class Response : IResponse + internal class Response : IResponse { - private readonly HttpWebResponse response; + private readonly HttpResponseMessage response; /// public HttpStatusCode StatusCode => response.StatusCode; + /// + public bool IsSuccessStatusCode => response.IsSuccessStatusCode; + + /// + public IEnumerable>> ResponseHeaders => response.Headers; + /// - /// Create response for http web response + /// Create response for a http response message /// - /// - public Response(HttpWebResponse response) + /// The actual response + public Response(HttpResponseMessage response) { this.response = response; } /// - public Stream GetResponseStream() + public async Task GetResponseStream() { - return response.GetResponseStream(); - } - - /// - public IEnumerable> GetResponseHeaders() - { - return response.Headers.ToIEnumerable(); + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } /// public void Close() { - response.Close(); + response.Dispose(); } } } diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index c73f25b..a635a73 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; +using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Web; using CryptoExchange.Net.Authentication; @@ -29,7 +29,6 @@ namespace CryptoExchange.Net /// public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); - /// /// Where to place post parameters /// @@ -66,18 +65,13 @@ namespace CryptoExchange.Net /// /// /// - protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider) + protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { - Configure(exchangeOptions); - } + if (exchangeOptions == null) + throw new ArgumentNullException("Options"); - /// - /// Configure the client using the provided options - /// - /// Options - protected void Configure(RestClientOptions exchangeOptions) - { RequestTimeout = exchangeOptions.RequestTimeout; + RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy); RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour; var rateLimiters = new List(); foreach (var rateLimiter in exchangeOptions.RateLimiters) @@ -91,6 +85,9 @@ namespace CryptoExchange.Net /// The limiter to add public void AddRateLimiter(IRateLimiter limiter) { + if (limiter == null) + throw new ArgumentNullException("limiter"); + var rateLimiters = RateLimiters.ToList(); rateLimiters.Add(limiter); RateLimiters = rateLimiters; @@ -132,6 +129,10 @@ namespace CryptoExchange.Net return new CallResult(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode }); return new CallResult(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message }); } + finally + { + ping.Dispose(); + } return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status }); } @@ -142,11 +143,13 @@ namespace CryptoExchange.Net /// The expected result type /// The uri to send the request to /// The method of the request + /// Cancellation token /// The parameters of the request /// Whether or not the request should be authenticated /// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) /// - protected virtual async Task> ExecuteRequest(Uri uri, string method = Constants.GetMethod, Dictionary parameters = null, bool signed = false, bool checkResult = true) where T : class + protected virtual async Task> SendRequest(Uri uri, HttpMethod method, CancellationToken cancellationToken, + Dictionary? parameters = null, bool signed = false, bool checkResult = true) where T : class { log.Write(LogVerbosity.Debug, "Creating request for " + uri); if (signed && authProvider == null) @@ -156,13 +159,6 @@ namespace CryptoExchange.Net } var request = ConstructRequest(uri, method, parameters, signed); - - if (apiProxy != null) - { - log.Write(LogVerbosity.Debug, "Setting proxy"); - request.SetProxy(apiProxy.Host, apiProxy.Port, apiProxy.Login, apiProxy.Password); - } - foreach (var limiter in RateLimiters) { var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour); @@ -176,37 +172,64 @@ namespace CryptoExchange.Net log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); } - string paramString = null; - if (parameters != null && method == Constants.PostMethod) - paramString = "with request body " + request.Content; + string? paramString = null; + if (parameters != null && method == HttpMethod.Post) + paramString = " with request body " + request.Content; - log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri} {paramString ?? ""}"); - var result = await ExecuteRequest(request).ConfigureAwait(false); - if(!result.Success) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, result.Error); - - var jsonResult = ValidateJson(result.Data); - if(!jsonResult.Success) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, jsonResult.Error); - - if (IsErrorResponse(jsonResult.Data)) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, ParseErrorResponse(jsonResult.Data)); - - var desResult = Deserialize(jsonResult.Data, checkResult); - if (!desResult.Success) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, desResult.Error); - - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, desResult.Data, null); + log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null? "": $" via proxy {apiProxy.Host}")}"); + return await GetResponse(request, cancellationToken).ConfigureAwait(false); } /// - /// Can be overridden to indicate if a response is an error response + /// Executes the request and returns the string result /// - /// The received data - /// True if error response - protected virtual bool IsErrorResponse(JToken data) + /// The request object to execute + /// Cancellation token + /// + private async Task> GetResponse(IRequest request, CancellationToken cancellationToken) { - return false; + try + { + TotalRequestsMade++; + var response = await request.GetResponse(cancellationToken).ConfigureAwait(false); + var statusCode = response.StatusCode; + var headers = response.ResponseHeaders; + var responseStream = await response.GetResponseStream().ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var desResult = await Deserialize(responseStream).ConfigureAwait(false); + response.Close(); + return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error); + } + else + { + using var reader = new StreamReader(responseStream); + var data = await reader.ReadToEndAsync().ConfigureAwait(false); + response.Close(); + var parseResult = ValidateJson(data); + return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) :new ServerError(data)); + } + } + catch (HttpRequestException requestException) + { + log.Write(LogVerbosity.Warning, "Request exception: " + requestException.Message); + return new WebCallResult(null, null, default, new ServerError(requestException.Message)); + } + catch (TaskCanceledException canceledException) + { + if(canceledException.CancellationToken == cancellationToken) + { + // Cancellation token cancelled + log.Write(LogVerbosity.Warning, "Request cancel requested"); + return new WebCallResult(null, null, default, new CancellationRequestedError()); + } + else + { + // Request timed out + log.Write(LogVerbosity.Warning, "Request timed out"); + return new WebCallResult(null, null, default, new WebError("Request timed out")); + } + } } /// @@ -217,7 +240,7 @@ namespace CryptoExchange.Net /// The parameters of the request /// Whether or not the request should be authenticated /// - protected virtual IRequest ConstructRequest(Uri uri, string method, Dictionary parameters, bool signed) + protected virtual IRequest ConstructRequest(Uri uri, HttpMethod method, Dictionary? parameters, bool signed) { if (parameters == null) parameters = new Dictionary(); @@ -226,57 +249,44 @@ namespace CryptoExchange.Net if(authProvider != null) parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed); - if((method == Constants.GetMethod || method == Constants.DeleteMethod || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true) + if((method == HttpMethod.Get || method == HttpMethod.Delete || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true) uriString += "?" + parameters.CreateParamString(true, arraySerialization); - - var request = RequestFactory.Create(uriString); - request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + + var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + var request = RequestFactory.Create(method, uriString); request.Accept = Constants.JsonContentHeader; - request.Method = method; var headers = new Dictionary(); if (authProvider != null) - headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters, signed); + headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed); foreach (var header in headers) request.Headers.Add(header.Key, header.Value); - if ((method == Constants.PostMethod || method == Constants.PutMethod) && postParametersPosition != PostParameters.InUri) + if ((method == HttpMethod.Post || method == HttpMethod.Put) && postParametersPosition != PostParameters.InUri) { if(parameters?.Any() == true) - WriteParamBody(request, parameters); - else - WriteParamBody(request, "{}"); + WriteParamBody(request, parameters, contentType); + else + request.SetContent("{}", contentType); } return request; } - /// - /// Writes the string data of the parameters to the request body stream - /// - /// - /// - protected virtual void WriteParamBody(IRequest request, string stringData) - { - var data = Encoding.UTF8.GetBytes(stringData); - request.ContentLength = data.Length; - request.Content = stringData; - using (var stream = request.GetRequestStream().Result) - stream.Write(data, 0, data.Length); - } - /// /// Writes the parameters of the request to the request object, either in the query string or the request body /// /// /// - protected virtual void WriteParamBody(IRequest request, Dictionary parameters) + /// + protected virtual void WriteParamBody(IRequest request, Dictionary parameters, string contentType) { if (requestBodyFormat == RequestBodyFormat.Json) { var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value)); - WriteParamBody(request, stringData); + request.SetContent(stringData, contentType); + } else if(requestBodyFormat == RequestBodyFormat.FormData) { @@ -284,81 +294,9 @@ namespace CryptoExchange.Net foreach (var kvp in parameters.OrderBy(p => p.Key)) formData.Add(kvp.Key, kvp.Value.ToString()); var stringData = formData.ToString(); - WriteParamBody(request, stringData); + request.SetContent(stringData, contentType); } - } - - /// - /// Executes the request and returns the string result - /// - /// The request object to execute - /// - private async Task> ExecuteRequest(IRequest request) - { - var returnedData = ""; - try - { - request.Timeout = RequestTimeout; - TotalRequestsMade++; - var response = await request.GetResponse().ConfigureAwait(false); - using (var reader = new StreamReader(response.GetResponseStream())) - { - returnedData = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Debug, "Data returned: " + returnedData); - } - - var statusCode = response.StatusCode; - var returnHeaders = response.GetResponseHeaders(); - response.Close(); - return new WebCallResult(statusCode, returnHeaders, returnedData, null); - } - catch (WebException we) - { - var response = (HttpWebResponse)we.Response; - var statusCode = response?.StatusCode; - var returnHeaders = response?.Headers.ToIEnumerable(); - - try - { - var responseStream = response?.GetResponseStream(); - if (response != null && responseStream != null) - { - using (var reader = new StreamReader(responseStream)) - { - returnedData = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Warning, "Server returned an error: " + returnedData); - } - - response.Close(); - - var jsonResult = ValidateJson(returnedData); - return !jsonResult.Success ? new WebCallResult(statusCode, returnHeaders, null, jsonResult.Error) : new WebCallResult(statusCode, returnHeaders, null, ParseErrorResponse(jsonResult.Data)); - } - } - catch (Exception e) - { - log.Write(LogVerbosity.Debug, "Not able to read server response: " + e.Message); - } - - var infoMessage = "No response from server"; - if (response == null) - { - infoMessage += $" | {we.Status} - {we.Message}"; - log.Write(LogVerbosity.Warning, infoMessage); - return new WebCallResult(0, null, null, new WebError(infoMessage)); - } - - infoMessage = $"Status: {response.StatusCode}-{response.StatusDescription}, Message: {we.Message}"; - log.Write(LogVerbosity.Warning, infoMessage); - response.Close(); - return new WebCallResult(statusCode, returnHeaders, null, new ServerError(infoMessage)); - } - catch (Exception e) - { - log.Write(LogVerbosity.Error, $"Unknown error occured: {e.GetType()}, {e.Message}, {e.StackTrace}"); - return new WebCallResult(null, null, null, new UnknownError(e.Message + ", data: " + returnedData)); - } - } + } /// /// Parse an error response from the server. Only used when server returns a status other than Success(200) diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index f2f8afd..ef99cfb 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -50,11 +50,11 @@ namespace CryptoExchange.Net /// /// Handler for byte data /// - protected Func dataInterpreterBytes; + protected Func? dataInterpreterBytes; /// /// Handler for string data /// - protected Func dataInterpreterString; + protected Func? dataInterpreterString; /// /// Generic handlers /// @@ -62,11 +62,11 @@ namespace CryptoExchange.Net /// /// Periodic task /// - protected Task periodicTask; + protected Task? periodicTask; /// /// Periodic task event /// - protected AutoResetEvent periodicEvent; + protected AutoResetEvent? periodicEvent; /// /// Is disposing /// @@ -84,17 +84,11 @@ namespace CryptoExchange.Net /// /// Client options /// Authentication provider - protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider) + protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { - Configure(exchangeOptions); - } + if (exchangeOptions == null) + throw new ArgumentNullException("Options"); - /// - /// Configure the client using the provided options - /// - /// Options - protected void Configure(SocketClientOptions exchangeOptions) - { AutoReconnect = exchangeOptions.AutoReconnect; ReconnectInterval = exchangeOptions.ReconnectInterval; ResponseTimeout = exchangeOptions.SocketResponseTimeout; @@ -137,7 +131,7 @@ namespace CryptoExchange.Net /// If the subscription should be authenticated /// The handler of update data /// - protected virtual async Task> Subscribe(string url, object request, string identifier, bool authenticated, Action dataHandler) + protected virtual async Task> Subscribe(string url, object? request, string? identifier, bool authenticated, Action dataHandler) { SocketConnection socket; SocketSubscription handler; @@ -155,7 +149,7 @@ namespace CryptoExchange.Net } var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); - if (!connectResult.Success) + if (!connectResult) return new CallResult(null, connectResult.Error); } finally @@ -170,7 +164,7 @@ namespace CryptoExchange.Net if (request != null) { var subResult = await SubscribeAndWait(socket, request, handler).ConfigureAwait(false); - if (!subResult.Success) + if (!subResult) { await socket.Close(handler).ConfigureAwait(false); return new CallResult(null, subResult.Error); @@ -192,8 +186,8 @@ namespace CryptoExchange.Net /// protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription) { - CallResult callResult = null; - await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false); + CallResult? callResult = null; + await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out var callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -237,7 +231,7 @@ namespace CryptoExchange.Net } var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); - if (!connectResult.Success) + if (!connectResult) return new CallResult(default, connectResult.Error); } finally @@ -291,17 +285,17 @@ namespace CryptoExchange.Net return new CallResult(true, null); var connectResult = await ConnectSocket(socket).ConfigureAwait(false); - if (!connectResult.Success) + if (!connectResult) return new CallResult(false, new CantConnectError()); if (!authenticated || socket.Authenticated) return new CallResult(true, null); var result = await AuthenticateSocket(socket).ConfigureAwait(false); - if (!result.Success) + if (!result) { log.Write(LogVerbosity.Warning, "Socket authentication failed"); - result.Error.Message = "Authentication failed: " + result.Error.Message; + result.Error!.Message = "Authentication failed: " + result.Error.Message; return new CallResult(false, result.Error); } @@ -377,7 +371,7 @@ namespace CryptoExchange.Net /// The socket connection the handler is on /// The handler of the data received /// - protected virtual SocketSubscription AddHandler(object request, string identifier, bool userSubscription, SocketConnection connection, Action dataHandler) + protected virtual SocketSubscription AddHandler(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action dataHandler) { void InternalHandler(SocketConnection socketWrapper, JToken data) { @@ -388,7 +382,7 @@ namespace CryptoExchange.Net } var desResult = Deserialize(data, false); - if (!desResult.Success) + if (!desResult) { log.Write(LogVerbosity.Warning, $"Failed to deserialize data into type {typeof(T)}: {desResult.Error}"); return; @@ -397,7 +391,7 @@ namespace CryptoExchange.Net dataHandler(desResult.Data); } - return connection.AddHandler(request ?? identifier, userSubscription, InternalHandler); + return connection.AddHandler(request ?? identifier!, userSubscription, InternalHandler); } /// @@ -486,6 +480,9 @@ namespace CryptoExchange.Net /// Method returning the object to send public virtual void SendPeriodic(TimeSpan interval, Func objGetter) { + if (objGetter == null) + throw new ArgumentNullException("objGetter"); + periodicEvent = new AutoResetEvent(false); periodicTask = Task.Run(async () => { @@ -517,7 +514,6 @@ namespace CryptoExchange.Net } } } - }); } @@ -530,7 +526,7 @@ namespace CryptoExchange.Net public virtual async Task Unsubscribe(UpdateSubscription subscription) { if (subscription == null) - return; + throw new ArgumentNullException("subscription"); log.Write(LogVerbosity.Info, "Closing subscription"); await subscription.Close().ConfigureAwait(false); @@ -564,9 +560,10 @@ namespace CryptoExchange.Net { disposing = true; periodicEvent?.Set(); + periodicEvent?.Dispose(); log.Write(LogVerbosity.Debug, "Disposing socket client, closing all subscriptions"); UnsubscribeAll().Wait(); - + semaphoreSlim?.Dispose(); base.Dispose(); } } diff --git a/CryptoExchange.Net/Sockets/BaseSocket.cs b/CryptoExchange.Net/Sockets/BaseSocket.cs index 0e8d5c6..e2a9aa5 100644 --- a/CryptoExchange.Net/Sockets/BaseSocket.cs +++ b/CryptoExchange.Net/Sockets/BaseSocket.cs @@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Sockets internal static int lastStreamId; private static readonly object streamIdLock = new object(); - protected WebSocket socket; + protected WebSocket? socket; protected Log log; protected object socketLock = new object(); @@ -32,34 +32,22 @@ namespace CryptoExchange.Net.Sockets protected IDictionary cookies; protected IDictionary headers; - protected HttpConnectProxy proxy; + protected HttpConnectProxy? proxy; public int Id { get; } public bool Reconnecting { get; set; } - public string Origin { get; set; } + public string? Origin { get; set; } public string Url { get; } - public bool IsClosed => socket.State == WebSocketState.Closed; - public bool IsOpen => socket.State == WebSocketState.Open; + public bool IsClosed => socket?.State == null ? true: socket.State == WebSocketState.Closed; + public bool IsOpen => socket?.State == WebSocketState.Open; public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; - public Func DataInterpreterBytes { get; set; } - public Func DataInterpreterString { get; set; } + public Func? DataInterpreterBytes { get; set; } + public Func? DataInterpreterString { get; set; } public DateTime LastActionTime { get; private set; } public TimeSpan Timeout { get; set; } - private Task timeoutTask; - - public bool PingConnection - { - get => socket.EnableAutoSendPing; - set => socket.EnableAutoSendPing = value; - } - - public TimeSpan PingInterval - { - get => TimeSpan.FromSeconds(socket.AutoSendPingInterval); - set => socket.AutoSendPingInterval = (int) Math.Round(value.TotalSeconds); - } + private Task? timeoutTask; public WebSocketState SocketState => socket?.State ?? WebSocketState.None; @@ -176,7 +164,7 @@ namespace CryptoExchange.Net.Sockets var waitLock = new object(); log?.Write(LogVerbosity.Debug, $"Socket {Id} closing"); - var evnt = new ManualResetEvent(false); + ManualResetEvent? evnt = new ManualResetEvent(false); var handler = new EventHandler((o, a) => { lock(waitLock) @@ -208,7 +196,7 @@ namespace CryptoExchange.Net.Sockets public virtual void Send(string data) { - socket.Send(data); + socket?.Send(data); } public virtual Task Connect() @@ -239,7 +227,7 @@ namespace CryptoExchange.Net.Sockets { log?.Write(LogVerbosity.Debug, $"Socket {Id} connecting"); var waitLock = new object(); - var evnt = new ManualResetEvent(false); + ManualResetEvent? evnt = new ManualResetEvent(false); var handler = new EventHandler((o, a) => { lock (waitLock) diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index a13585e..4c8443b 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -19,15 +19,15 @@ namespace CryptoExchange.Net.Sockets /// /// Connection lost event /// - public event Action ConnectionLost; + public event Action? ConnectionLost; /// /// Connecting restored event /// - public event Action ConnectionRestored; + public event Action? ConnectionRestored; /// /// Connecting closed event /// - public event Action Closed; + public event Action? Closed; /// /// The amount of handlers @@ -119,7 +119,7 @@ namespace CryptoExchange.Net.Sockets /// public SocketSubscription AddHandler(object request, bool userSubscription, Action dataHandler) { - var handler = new SocketSubscription(null, request, userSubscription, dataHandler); + var handler = new SocketSubscription(request, userSubscription, dataHandler); lock (handlersLock) handlers.Add(handler); return handler; @@ -135,7 +135,7 @@ namespace CryptoExchange.Net.Sockets /// public SocketSubscription AddHandler(string identifier, bool userSubscription, Action dataHandler) { - var handler = new SocketSubscription(identifier, null, userSubscription, dataHandler); + var handler = new SocketSubscription(identifier, userSubscription, dataHandler); lock (handlersLock) handlers.Add(handler); return handler; @@ -169,7 +169,7 @@ namespace CryptoExchange.Net.Sockets private bool HandleData(JToken tokenData) { - SocketSubscription currentSubscription = null; + SocketSubscription? currentSubscription = null; try { var handled = false; @@ -181,7 +181,7 @@ namespace CryptoExchange.Net.Sockets currentSubscription = handler; if (handler.Request == null) { - if (socketClient.MessageMatchesHandler(tokenData, handler.Identifier)) + if (socketClient.MessageMatchesHandler(tokenData, handler.Identifier!)) { handled = true; handler.MessageHandler(this, tokenData); @@ -326,7 +326,7 @@ namespace CryptoExchange.Net.Sockets if (Authenticated) { var authResult = await socketClient.AuthenticateSocket(this).ConfigureAwait(false); - if (!authResult.Success) + if (!authResult) { log.Write(LogVerbosity.Info, "Authentication failed on reconnected socket. Disconnecting and reconnecting."); return false; @@ -343,9 +343,9 @@ namespace CryptoExchange.Net.Sockets var taskList = new List(); foreach (var handler in handlerList) { - var task = socketClient.SubscribeAndWait(this, handler.Request, handler).ContinueWith(t => + var task = socketClient.SubscribeAndWait(this, handler.Request!, handler).ContinueWith(t => { - if (!t.Result.Success) + if (!t.Result) success = false; }); taskList.Add(task); @@ -403,7 +403,7 @@ namespace CryptoExchange.Net.Sockets internal class PendingRequest { public Func Handler { get; } - public JToken Result { get; private set; } + public JToken? Result { get; private set; } public ManualResetEvent Event { get; } public TimeSpan Timeout { get; } diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index 0de0546..0f1df22 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Sockets /// /// Exception event /// - public event Action Exception; + public event Action? Exception; /// /// Message handlers for this subscription. Should return true if the message is handled and should not be distributed to the other handlers @@ -21,11 +21,11 @@ namespace CryptoExchange.Net.Sockets /// /// Request object /// - public object Request { get; set; } + public object? Request { get; set; } /// /// Subscription identifier /// - public string Identifier { get; set; } + public string? Identifier { get; set; } /// /// Is user subscription or generic /// @@ -36,22 +36,32 @@ namespace CryptoExchange.Net.Sockets /// public bool Confirmed { get; set; } + /// + /// ctor + /// + /// + /// + /// + public SocketSubscription(object request, bool userSubscription, Action dataHandler) + { + UserSubscription = userSubscription; + MessageHandler = dataHandler; + Request = request; + } /// /// ctor /// /// - /// /// /// - public SocketSubscription(string identifier, object request, bool userSubscription, Action dataHandler) + public SocketSubscription(string identifier, bool userSubscription, Action dataHandler) { UserSubscription = userSubscription; MessageHandler = dataHandler; Identifier = identifier; - Request = request; } - + /// /// Invoke the exception event /// From b2b5b0fef0b55d80152534a66d45af0f08053655 Mon Sep 17 00:00:00 2001 From: JKorf Date: Sun, 20 Oct 2019 13:36:38 +0200 Subject: [PATCH 12/41] wip --- .../BaseClientTests.cs | 6 +- .../RestClientTests.cs | 9 +- .../SocketClientTests.cs | 18 +- .../TestImplementations/TestBaseClient.cs | 7 +- .../TestImplementations/TestRestClient.cs | 50 ++- .../TestImplementations/TestSocketClient.cs | 15 +- CryptoExchange.Net/AssemblyInfo.cs | 1 + .../Attributes/NullableAttributes.cs | 210 +++++++++++++ CryptoExchange.Net/BaseClient.cs | 30 +- .../Converters/BaseConverter.cs | 2 +- CryptoExchange.Net/CryptoExchange.Net.csproj | 4 +- CryptoExchange.Net/CryptoExchange.Net.xml | 287 +++++++++++++++--- CryptoExchange.Net/ExtensionMethods.cs | 2 +- CryptoExchange.Net/Interfaces/IRequest.cs | 12 +- CryptoExchange.Net/Interfaces/IRestClient.cs | 5 +- .../Interfaces/ISocketClient.cs | 15 + .../Logging/ThreadSafeFileWriter.cs | 2 +- CryptoExchange.Net/Objects/CallResult.cs | 12 +- CryptoExchange.Net/Objects/Error.cs | 39 ++- CryptoExchange.Net/Objects/Options.cs | 4 +- .../OrderBook/SymbolOrderBook.cs | 26 +- CryptoExchange.Net/Requests/Request.cs | 14 +- CryptoExchange.Net/Requests/RequestFactory.cs | 3 +- CryptoExchange.Net/RestClient.cs | 27 +- CryptoExchange.Net/SocketClient.cs | 50 +-- CryptoExchange.Net/Sockets/BaseSocket.cs | 129 +++++++- .../Sockets/SocketConnection.cs | 45 +-- .../Sockets/SocketSubscription.cs | 32 +- 28 files changed, 819 insertions(+), 237 deletions(-) create mode 100644 CryptoExchange.Net/AssemblyInfo.cs create mode 100644 CryptoExchange.Net/Attributes/NullableAttributes.cs diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index bf43577..c109479 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -23,7 +23,7 @@ namespace CryptoExchange.Net.UnitTests // arrange // act // assert - Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions() { ApiCredentials = new ApiCredentials(key, secret) })); + Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) })); } [TestCase] @@ -31,7 +31,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var stringBuilder = new StringBuilder(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new RestClientOptions("") { LogWriters = new List { new StringWriter(stringBuilder) } }); @@ -67,7 +67,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var stringBuilder = new StringBuilder(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new RestClientOptions("") { LogWriters = new List { new StringWriter(stringBuilder) }, LogVerbosity = verbosity diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 78fa402..963e4f4 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.RateLimiter; @@ -105,7 +106,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange // act - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { BaseAddress = "http://test.address.com", RateLimiters = new List{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))}, @@ -123,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Fail @@ -146,7 +147,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait @@ -171,7 +172,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait, diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index 42c7ae1..e7d4049 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.UnitTests { //arrange //act - var client = new TestSocketClient(new SocketClientOptions() + var client = new TestSocketClient(new SocketClientOptions("") { BaseAddress = "http://test.address.com", ReconnectInterval = TimeSpan.FromSeconds(6) @@ -51,7 +51,7 @@ namespace CryptoExchange.Net.UnitTests public void SocketMessages_Should_BeProcessedInDataHandlers() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -59,11 +59,11 @@ namespace CryptoExchange.Net.UnitTests var sub = new SocketConnection(client, socket); var rstEvent = new ManualResetEvent(false); JToken result = null; - sub.AddHandler("TestHandler", true, (connection, data) => + sub.AddHandler(SocketSubscription.CreateForIdentifier("TestHandler", true, (connection, data) => { result = data; rstEvent.Set(); - }); + })); client.ConnectSocketSub(sub); // act @@ -79,7 +79,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange bool reconnected = false; - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -106,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingStream_Should_CloseTheSocket() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.CanConnect = true; var sub = new SocketConnection(client, socket); client.ConnectSocketSub(sub); - var ups = new UpdateSubscription(sub, new SocketSubscription("Test", null, true, (d, a) => {})); + var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (d, a) => {})); // act client.Unsubscribe(ups).Wait(); @@ -124,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingAll_Should_CloseAllSockets() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket1 = client.CreateSocket(); var socket2 = client.CreateSocket(); socket1.CanConnect = true; @@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests public void FailingToConnectSocket_Should_ReturnError() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.CanConnect = false; var sub = new SocketConnection(client, socket); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index dc0cd25..9b36f42 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; @@ -7,7 +8,7 @@ namespace CryptoExchange.Net.UnitTests { public class TestBaseClient: BaseClient { - public TestBaseClient(): base(new RestClientOptions(), null) + public TestBaseClient(): base(new RestClientOptions("http://testurl.url"), null) { } @@ -37,12 +38,12 @@ namespace CryptoExchange.Net.UnitTests { } - public override Dictionary AddAuthenticationToHeaders(string uri, string method, Dictionary parameters, bool signed) + public override Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed) { return base.AddAuthenticationToHeaders(uri, method, parameters, signed); } - public override Dictionary AddAuthenticationToParameters(string uri, string method, Dictionary parameters, bool signed) + public override Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed) { return base.AddAuthenticationToParameters(uri, method, parameters, signed); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index e8bf674..8364895 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -7,8 +7,10 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; @@ -16,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestRestClient: RestClient { - public TestRestClient() : base(new RestClientOptions(), null) + public TestRestClient() : base(new RestClientOptions("http://testurl.url"), null) { RequestFactory = new Mock().Object; } @@ -39,36 +41,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations responseStream.Seek(0, SeekOrigin.Begin); var response = new Mock(); - response.Setup(c => c.GetResponseStream()).Returns(responseStream); - + response.Setup(c => c.IsSuccessStatusCode).Returns(true); + response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetRequestStream()).Returns(Task.FromResult(requestStream)); - request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object)); + request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } public void SetErrorWithoutResponse(HttpStatusCode code, string message) { - var we = new WebException(); - var r = new HttpWebResponse(); - var re = new HttpResponseMessage(); - - typeof(HttpResponseMessage).GetField("_statusCode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(re, code); - typeof(HttpWebResponse).GetField("_httpResponseMessage", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(r, re); - typeof(WebException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); - typeof(WebException).GetField("_response", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, r); - + var we = new HttpRequestException(); + typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); - request.Setup(c => c.GetResponse()).Throws(we); + request.Setup(c => c.GetResponse(It.IsAny())).Throws(we); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } @@ -79,22 +73,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations responseStream.Write(expectedBytes, 0, expectedBytes.Length); responseStream.Seek(0, SeekOrigin.Begin); - var r = new Mock(); - r.Setup(x => x.GetResponseStream()).Returns(responseStream); - var we = new WebException("", null, WebExceptionStatus.Success, r.Object); - + var response = new Mock(); + response.Setup(c => c.IsSuccessStatusCode).Returns(false); + response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); - request.Setup(c => c.GetResponse()).Throws(we); + request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); + request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } - public async Task> Request(string method = "GET") where T:class + public async Task> Request(CancellationToken ct = default) where T:class { - return await ExecuteRequest(new Uri("http://www.test.com"), method); + return await SendRequest(new Uri("http://www.test.com"), HttpMethod.Get, ct); } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index d01dc94..43f18b9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -11,7 +11,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestSocketClient: SocketClient { - public TestSocketClient() : this(new SocketClientOptions()) + public TestSocketClient() : this(new SocketClientOptions("http://testurl.url")) { } @@ -32,32 +32,33 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations return ConnectSocket(sub).Result; } - protected override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) + protected internal override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) { throw new NotImplementedException(); } - protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult callResult) + protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, + out CallResult callResult) { throw new NotImplementedException(); } - protected override bool MessageMatchesHandler(JToken message, object request) + protected internal override bool MessageMatchesHandler(JToken message, object request) { throw new NotImplementedException(); } - protected override bool MessageMatchesHandler(JToken message, string identifier) + protected internal override bool MessageMatchesHandler(JToken message, string identifier) { return true; } - protected override Task> AuthenticateSocket(SocketConnection s) + protected internal override Task> AuthenticateSocket(SocketConnection s) { throw new NotImplementedException(); } - protected override Task Unsubscribe(SocketConnection connection, SocketSubscription s) + protected internal override Task Unsubscribe(SocketConnection connection, SocketSubscription s) { throw new NotImplementedException(); } diff --git a/CryptoExchange.Net/AssemblyInfo.cs b/CryptoExchange.Net/AssemblyInfo.cs new file mode 100644 index 0000000..987163f --- /dev/null +++ b/CryptoExchange.Net/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")] \ No newline at end of file diff --git a/CryptoExchange.Net/Attributes/NullableAttributes.cs b/CryptoExchange.Net/Attributes/NullableAttributes.cs new file mode 100644 index 0000000..cb75272 --- /dev/null +++ b/CryptoExchange.Net/Attributes/NullableAttributes.cs @@ -0,0 +1,210 @@ +#if !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using global::System; + + /// + /// Specifies that is allowed as an input even if the + /// corresponding type disallows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class AllowNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public AllowNullAttribute() { } + } + + /// + /// Specifies that is disallowed as an input even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + + [ExcludeFromCodeCoverage] + internal sealed class DisallowNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DisallowNullAttribute() { } + } + + /// + /// Specifies that a method that will never return under any circumstance. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class DoesNotReturnAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DoesNotReturnAttribute() { } + } + + /// + /// Specifies that the method will not return if the associated + /// parameter is passed the specified value. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// + /// Gets the condition parameter value. + /// Code after the method is considered unreachable by diagnostics if the argument + /// to the associated parameter matches this value. + /// + public bool ParameterValue { get; } + + /// + /// Initializes a new instance of the + /// class with the specified parameter value. + /// + /// + /// The condition parameter value. + /// Code after the method is considered unreachable by diagnostics if the argument + /// to the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) + { + ParameterValue = parameterValue; + } + } + + /// + /// Specifies that an output may be even if the + /// corresponding type disallows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class MaybeNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public MaybeNullAttribute() { } + } + + /// + /// Specifies that when a method returns , + /// the parameter may be even if the corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter may be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter may be . + /// + public MaybeNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + } + + /// + /// Specifies that an output is not even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class NotNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public NotNullAttribute() { } + } + + /// + /// Specifies that the output will be non- if the + /// named parameter is non-. + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + AllowMultiple = true, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// + /// Gets the associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public string ParameterName { get; } + + /// + /// Initializes the attribute with the associated parameter name. + /// + /// + /// The associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public NotNullIfNotNullAttribute(string parameterName) + { + // .NET Core 3.0 doesn't throw an ArgumentNullException, even though this is + // tagged as non-null. + // Follow this behavior here. + ParameterName = parameterName; + } + } + + /// + /// Specifies that when a method returns , + /// the parameter will not be even if the corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class NotNullWhenAttribute : Attribute + { + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + } +} +#endif \ No newline at end of file diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 699f349..aa475b6 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; namespace CryptoExchange.Net @@ -22,7 +23,7 @@ namespace CryptoExchange.Net /// /// The address of the client /// - public string BaseAddress { get; private set; } + public string BaseAddress { get; } /// /// The log object /// @@ -207,36 +208,39 @@ namespace CryptoExchange.Net try { - using var reader = new StreamReader(stream); + using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); using var jsonReader = new JsonTextReader(reader); return new CallResult(serializer.Deserialize(jsonReader), null); } catch (JsonReaderException jre) { + if(stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); var data = await ReadStream(stream).ConfigureAwait(false); - var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"; - log.Write(LogVerbosity.Error, info); - return new CallResult(default, new DeserializeError(info)); + log.Write(LogVerbosity.Error, $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); } catch (JsonSerializationException jse) { + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); var data = await ReadStream(stream).ConfigureAwait(false); - var info = $"Deserialize JsonSerializationException: {jse.Message}, data: {data}"; - log.Write(LogVerbosity.Error, info); - return new CallResult(default, new DeserializeError(info)); + log.Write(LogVerbosity.Error, $"Deserialize JsonSerializationException: {jse.Message}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); } catch (Exception ex) { + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); var data = await ReadStream(stream).ConfigureAwait(false); - var info = $"Deserialize Unknown Exception: {ex.Message}, data: {data}"; - log.Write(LogVerbosity.Error, info); - return new CallResult(default, new DeserializeError(info)); + log.Write(LogVerbosity.Error, $"Deserialize Unknown Exception: {ex.Message}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); } } private async Task ReadStream(Stream stream) - { - using var reader = new StreamReader(stream); + { + using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); return await reader.ReadToEndAsync().ConfigureAwait(false); } diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index 97f18d0..da8e3d5 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -70,7 +70,7 @@ namespace CryptoExchange.Net.Converters return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T); } - private bool GetValue(string value, [NotNullWhen(false)]out T result) + private bool GetValue(string value, out T result) { var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); if (!mapping.Equals(default(KeyValuePair))) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 783ef3e..99af2ea 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,6 +1,6 @@ - netstandard2.1 + netstandard2.0;netstandard2.1 CryptoExchange.Net @@ -8,12 +8,12 @@ 2.1.8 false https://github.com/JKorf/CryptoExchange.Net - https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE en true 2.1.8 - Added array serialization options for implementations enable 8.0 + MIT CryptoExchange.Net.xml diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 0a8433e..d71e803 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -536,11 +536,6 @@ Content - - - Headers - - Method @@ -564,6 +559,13 @@ + + + Add a header to the request + + + + Get the response @@ -663,13 +665,13 @@ Removes all rate limiters from this client - + Ping to see if the server is reachable The roundtrip time of the ping request - + Ping to see if the server is reachable @@ -700,6 +702,20 @@ The base address of the API + + + + + + + + + The max amount of concurrent socket connections + + + + + Unsubscribe from a stream @@ -1135,6 +1151,12 @@ + + + Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) + + + The result of a request @@ -1306,12 +1328,18 @@ The message for the error that occured - + + + Optional data for the error + + + ctor + @@ -1344,51 +1372,53 @@ Error returned by the server - + ctor + - + ctor + Web error returned by the server - + ctor - + Error while deserializing data - + ctor - + Deserializing data Unknown error - + ctor - + Error data @@ -1665,6 +1695,11 @@ The bid list + + + Order book implementation id + + The log @@ -1910,9 +1945,6 @@ - - - @@ -1928,6 +1960,9 @@ + + + @@ -2034,13 +2069,13 @@ Removes all rate limiters from this client - + Ping to see if the server is reachable The roundtrip time of the ping request - + Ping to see if the server is reachable @@ -2380,6 +2415,187 @@ Socket implementation + + + Socket + + + + + Log + + + + + Error handlers + + + + + Open handlers + + + + + Close handlers + + + + + Message handlers + + + + + Id + + + + + If is reconnecting + + + + + Origin + + + + + Url + + + + + Is closed + + + + + Is open + + + + + Protocols + + + + + Interpreter for bytes + + + + + Interpreter for strings + + + + + Last action time + + + + + Timeout + + + + + Socket state + + + + + ctor + + + + + + + ctor + + + + + + + + + On close + + + + + On message + + + + + On error + + + + + On open + + + + + Handle + + + + + + Handle + + + + + + + + Checks if timed out + + + + + + Close socket + + + + + + Reset socket + + + + + Send data + + + + + + Connect socket + + + + + + Set a proxy + + + + + + + Dispose + + Socket connecting @@ -2442,24 +2658,11 @@ The socket client The socket - + - Add a handler + Add handler - The request object - If it is a user subscription or a generic handler - The data handler - - - - - Add a handler - - The identifier of the handler - If it is a user subscription or a generic handler - The data handler - - + @@ -2538,21 +2741,23 @@ If the subscription has been confirmed - + - ctor + Create SocketSubscription for a request + - + - ctor + Create SocketSubscription for an identifier + diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index b8133b4..116a904 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -131,7 +131,7 @@ namespace CryptoExchange.Net /// /// /// - internal static SecureString ToSecureString(this string source) + public static SecureString ToSecureString(this string source) { var secureString = new SecureString(); foreach (var c in source) diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 656a276..c8b9403 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Threading; @@ -20,10 +21,6 @@ namespace CryptoExchange.Net.Interfaces /// string? Content { get; } /// - /// Headers - /// - HttpRequestHeaders Headers { get; } - /// /// Method /// HttpMethod Method { get; set; } @@ -42,6 +39,13 @@ namespace CryptoExchange.Net.Interfaces /// /// void SetContent(string data, string contentType); + + /// + /// Add a header to the request + /// + /// + /// + void AddHeader(string key, string value); /// /// Get the response /// diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index c2f3b82..60d9e8b 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; @@ -51,12 +52,12 @@ namespace CryptoExchange.Net.Interfaces /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - CallResult Ping(); + CallResult Ping(CancellationToken ct = default); /// /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - Task> PingAsync(); + Task> PingAsync(CancellationToken ct = default); } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 195b1ce..7cbacc9 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; namespace CryptoExchange.Net.Interfaces @@ -29,6 +30,20 @@ namespace CryptoExchange.Net.Interfaces /// string BaseAddress { get; } + /// + TimeSpan ResponseTimeout { get; } + + /// + TimeSpan SocketNoDataTimeout { get; } + + /// + /// The max amount of concurrent socket connections + /// + int MaxSocketConnections { get; } + + /// + int SocketCombineTarget { get; } + /// /// Unsubscribe from a stream /// diff --git a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs index 97c6ce0..a9556dc 100644 --- a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs +++ b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs @@ -12,7 +12,7 @@ namespace CryptoExchange.Net.Logging private static readonly object openedFilesLock = new object(); private static readonly List openedFiles = new List(); - private StreamWriter logWriter; + private readonly StreamWriter logWriter; private readonly object writeLock; /// diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 78044f4..9253ead 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Objects /// public static implicit operator bool(CallResult obj) { - return !ReferenceEquals(obj, null) && obj.Success; + return obj?.Success == true; } } @@ -67,7 +67,9 @@ namespace CryptoExchange.Net.Objects /// /// /// - public WebCallResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) + public WebCallResult( + HttpStatusCode? code, + IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) { ResponseHeaders = responseHeaders; ResponseStatusCode = code; @@ -80,7 +82,7 @@ namespace CryptoExchange.Net.Objects /// public static WebCallResult CreateErrorResult(Error error) { - return new WebCallResult(null, null, default, error); + return new WebCallResult(null, null, default!, error); } /// @@ -90,9 +92,9 @@ namespace CryptoExchange.Net.Objects /// /// /// - public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>> responseHeaders, Error error) + public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, Error error) { - return new WebCallResult(code, responseHeaders, default, error); + return new WebCallResult(code, responseHeaders, default!, error); } } } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 0836d3e..0a4b838 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -14,15 +14,22 @@ /// public string Message { get; set; } + /// + /// Optional data for the error + /// + public object? Data { get; set; } + /// /// ctor /// /// /// - protected Error(int code, string message) + /// + protected Error(int code, string message, object? data) { Code = code; Message = message; + Data = data; } /// @@ -31,7 +38,7 @@ /// public override string ToString() { - return $"{Code}: {Message}"; + return $"{Code}: {Message} {Data}"; } } @@ -43,7 +50,7 @@ /// /// ctor /// - public CantConnectError() : base(1, "Can't connect to the server") { } + public CantConnectError() : base(1, "Can't connect to the server", null) { } } /// @@ -54,7 +61,7 @@ /// /// ctor /// - public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint") { } + public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint", null) { } } /// @@ -66,14 +73,16 @@ /// ctor /// /// - public ServerError(string message) : base(3, "Server error: " + message) { } + /// + public ServerError(string message, object? data = null) : base(3, "Server error: " + message, data) { } /// /// ctor /// /// /// - public ServerError(int code, string message) : base(code, message) + /// + public ServerError(int code, string message, object? data = null) : base(code, message, data) { } } @@ -86,8 +95,8 @@ /// /// ctor /// - /// - public WebError(string message) : base(4, "Web error: " + message) { } + /// + public WebError(object? data) : base(4, "Web error", data) { } } /// @@ -98,8 +107,8 @@ /// /// ctor /// - /// - public DeserializeError(string message) : base(5, "Error deserializing data: " + message) { } + /// Deserializing data + public DeserializeError(object? data) : base(5, "Error deserializing data", data) { } } /// @@ -110,8 +119,8 @@ /// /// ctor /// - /// - public UnknownError(string message) : base(6, "Unknown error occured " + message) { } + /// Error data + public UnknownError(object? data = null) : base(6, "Unknown error occured", data) { } } /// @@ -123,7 +132,7 @@ /// ctor /// /// - public ArgumentError(string message) : base(7, "Invalid parameter: " + message) { } + public ArgumentError(string message) : base(7, "Invalid parameter: " + message, null) { } } /// @@ -135,7 +144,7 @@ /// ctor /// /// - public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { } + public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message, null) { } } /// @@ -146,6 +155,6 @@ /// /// ctor /// - public CancellationRequestedError() : base(9, "Cancellation requested") { } + public CancellationRequestedError() : base(9, "Cancellation requested", null) { } } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index e7e67c7..18744ab 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -153,7 +153,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, RateLimitters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout.ToString("c")}"; + return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}"; } } @@ -223,7 +223,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout.ToString("c")}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; + return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; } } } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 65aab30..675e8e8 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -33,7 +33,11 @@ namespace CryptoExchange.Net.OrderBook private OrderBookStatus status; private UpdateSubscription? subscription; private readonly bool sequencesAreConsecutive; - private readonly string id; + + /// + /// Order book implementation id + /// + public string Id { get; } /// /// The log /// @@ -54,7 +58,7 @@ namespace CryptoExchange.Net.OrderBook var old = status; status = value; - log.Write(LogVerbosity.Info, $"{id} order book {Symbol} status changed: {old} => {value}"); + log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} status changed: {old} => {value}"); OnStatusChange?.Invoke(old, status); } } @@ -146,12 +150,12 @@ namespace CryptoExchange.Net.OrderBook protected SymbolOrderBook(string symbol, OrderBookOptions options) { if (symbol == null) - throw new ArgumentNullException("symbol"); + throw new ArgumentNullException(nameof(symbol)); if (options == null) - throw new ArgumentNullException("options"); + throw new ArgumentNullException(nameof(options)); - id = options.OrderBookName; + Id = options.OrderBookName; processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; Symbol = symbol; @@ -191,7 +195,7 @@ namespace CryptoExchange.Net.OrderBook private void Reset() { - log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} connection lost"); + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost"); Status = OrderBookStatus.Connecting; processBuffer.Clear(); bookSet = false; @@ -211,7 +215,7 @@ namespace CryptoExchange.Net.OrderBook success = resyncResult; } - log.Write(LogVerbosity.Info, $"{id} order book {Symbol} successfully resynchronized"); + log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} successfully resynchronized"); Status = OrderBookStatus.Synced; } @@ -278,7 +282,7 @@ namespace CryptoExchange.Net.OrderBook bookSet = true; LastOrderBookUpdate = DateTime.UtcNow; OnOrderBookUpdate?.Invoke(); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } @@ -304,12 +308,12 @@ namespace CryptoExchange.Net.OrderBook Entries = entries }; processBuffer.Add(entry); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update before synced; buffering"); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update before synced; buffering"); } else if (sequencesAreConsecutive && firstSequenceNumber > LastSequenceNumber + 1) { // Out of sync - log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} out of sync, reconnecting"); + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync, reconnecting"); subscription!.Reconnect().Wait(); } else @@ -320,7 +324,7 @@ namespace CryptoExchange.Net.OrderBook CheckProcessBuffer(); LastOrderBookUpdate = DateTime.UtcNow; OnOrderBookUpdate?.Invoke(); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update: {entries.Count} entries processed"); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update: {entries.Count} entries processed"); } } } diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 4edbd5f..3387cde 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -11,7 +12,7 @@ namespace CryptoExchange.Net.Requests /// /// Request object /// - internal class Request : IRequest + public class Request : IRequest { private readonly HttpRequestMessage request; private readonly HttpClient httpClient; @@ -26,10 +27,7 @@ namespace CryptoExchange.Net.Requests httpClient = client; this.request = request; } - - /// - public HttpRequestHeaders Headers => request.Headers; - + /// public string? Content { get; private set; } @@ -56,6 +54,12 @@ namespace CryptoExchange.Net.Requests request.Content = new StringContent(data, Encoding.UTF8, contentType); } + /// + public void AddHeader(string key, string value) + { + request.Headers.Add(key, value); + } + /// public void SetContent(byte[] data) { diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index b91e188..2434ca1 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -25,8 +25,7 @@ namespace CryptoExchange.Net.Requests } }; - httpClient = new HttpClient(handler); - httpClient.Timeout = requestTimeout; + httpClient = new HttpClient(handler) {Timeout = requestTimeout}; } /// diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index a635a73..11aa854 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; @@ -46,11 +47,11 @@ namespace CryptoExchange.Net /// /// Timeout for requests /// - protected TimeSpan RequestTimeout { get; private set; } + protected TimeSpan RequestTimeout { get; } /// /// Rate limiting behaviour /// - public RateLimitingBehaviour RateLimitBehaviour { get; private set; } + public RateLimitingBehaviour RateLimitBehaviour { get; } /// /// List of ratelimitters /// @@ -68,7 +69,7 @@ namespace CryptoExchange.Net protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { if (exchangeOptions == null) - throw new ArgumentNullException("Options"); + throw new ArgumentNullException(nameof(exchangeOptions)); RequestTimeout = exchangeOptions.RequestTimeout; RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy); @@ -86,7 +87,7 @@ namespace CryptoExchange.Net public void AddRateLimiter(IRateLimiter limiter) { if (limiter == null) - throw new ArgumentNullException("limiter"); + throw new ArgumentNullException(nameof(limiter)); var rateLimiters = RateLimiters.ToList(); rateLimiters.Add(limiter); @@ -105,17 +106,19 @@ namespace CryptoExchange.Net /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - public virtual CallResult Ping() => PingAsync().Result; + public virtual CallResult Ping(CancellationToken ct = default) => PingAsync(ct).Result; /// /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - public virtual async Task> PingAsync() + public virtual async Task> PingAsync(CancellationToken ct = default) { var ping = new Ping(); var uri = new Uri(BaseAddress); PingReply reply; + + var ctRegistration = ct.Register(() => ping.SendAsyncCancel()); try { reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false); @@ -131,9 +134,13 @@ namespace CryptoExchange.Net } finally { + ctRegistration.Dispose(); ping.Dispose(); } + if(ct.IsCancellationRequested) + return new CallResult(0, new CancellationRequestedError()); + return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status }); } @@ -148,6 +155,7 @@ namespace CryptoExchange.Net /// Whether or not the request should be authenticated /// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) /// + [return: NotNull] protected virtual async Task> SendRequest(Uri uri, HttpMethod method, CancellationToken cancellationToken, Dictionary? parameters = null, bool signed = false, bool checkResult = true) where T : class { @@ -198,13 +206,17 @@ namespace CryptoExchange.Net if (response.IsSuccessStatusCode) { var desResult = await Deserialize(responseStream).ConfigureAwait(false); + responseStream.Close(); response.Close(); + + return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error); } else { using var reader = new StreamReader(responseStream); var data = await reader.ReadToEndAsync().ConfigureAwait(false); + responseStream.Close(); response.Close(); var parseResult = ValidateJson(data); return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) :new ServerError(data)); @@ -261,7 +273,7 @@ namespace CryptoExchange.Net headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed); foreach (var header in headers) - request.Headers.Add(header.Key, header.Value); + request.AddHeader(header.Key, header.Value); if ((method == HttpMethod.Post || method == HttpMethod.Put) && postParametersPosition != PostParameters.InUri) { @@ -286,7 +298,6 @@ namespace CryptoExchange.Net { var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value)); request.SetContent(stringData, contentType); - } else if(requestBodyFormat == RequestBodyFormat.FormData) { diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index ef99cfb..b416553 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -33,13 +34,13 @@ namespace CryptoExchange.Net protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); /// - public TimeSpan ReconnectInterval { get; private set; } + public TimeSpan ReconnectInterval { get; } /// - public bool AutoReconnect { get; private set; } + public bool AutoReconnect { get; } /// - public TimeSpan ResponseTimeout { get; private set; } + public TimeSpan ResponseTimeout { get; } /// - public TimeSpan SocketNoDataTimeout { get; private set; } + public TimeSpan SocketNoDataTimeout { get; } /// /// The max amount of concurrent socket connections /// @@ -87,7 +88,7 @@ namespace CryptoExchange.Net protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { if (exchangeOptions == null) - throw new ArgumentNullException("Options"); + throw new ArgumentNullException(nameof(exchangeOptions)); AutoReconnect = exchangeOptions.AutoReconnect; ReconnectInterval = exchangeOptions.ReconnectInterval; @@ -101,7 +102,7 @@ namespace CryptoExchange.Net /// /// Handler for byte data /// Handler for string data - protected void SetDataInterpreter(Func byteHandler, Func stringHandler) + protected void SetDataInterpreter(Func? byteHandler, Func? stringHandler) { dataInterpreterBytes = byteHandler; dataInterpreterString = stringHandler; @@ -116,7 +117,7 @@ namespace CryptoExchange.Net /// If the subscription should be authenticated /// The handler of update data /// - protected virtual Task> Subscribe(object request, string identifier, bool authenticated, Action dataHandler) + protected virtual Task> Subscribe(object? request, string? identifier, bool authenticated, Action dataHandler) { return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler); } @@ -171,8 +172,10 @@ namespace CryptoExchange.Net } } else + { handler.Confirmed = true; - + } + socket.ShouldReconnect = true; return new CallResult(new UpdateSubscription(socket, handler), null); } @@ -187,7 +190,7 @@ namespace CryptoExchange.Net protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription) { CallResult? callResult = null; - await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out var callResult)).ConfigureAwait(false); + await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -312,7 +315,7 @@ namespace CryptoExchange.Net /// The message /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the query - protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult); + protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, [NotNullWhen(true)]out CallResult? callResult); /// /// Needs to check if a received message was an answer to a subscription request (preferable by id) and set the callResult out to whatever the response is /// @@ -322,7 +325,7 @@ namespace CryptoExchange.Net /// The message /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the subscription request - protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult callResult); + protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult? callResult); /// /// Needs to check if a received message matches a handler. Typically if an update message matches the request /// @@ -391,7 +394,11 @@ namespace CryptoExchange.Net dataHandler(desResult.Data); } - return connection.AddHandler(request ?? identifier!, userSubscription, InternalHandler); + var handler = request == null + ? SocketSubscription.CreateForIdentifier(identifier!, userSubscription, InternalHandler) + : SocketSubscription.CreateForRequest(request, userSubscription, InternalHandler); + connection.AddHandler(handler); + return handler; } /// @@ -399,11 +406,12 @@ namespace CryptoExchange.Net /// /// The name of the request handler. Needs to be unique /// The action to execute when receiving a message for this handler (checked by ) - protected virtual void AddGenericHandler(string identifier, Action action) + protected void AddGenericHandler(string identifier, Action action) { genericHandlers.Add(identifier, action); + var handler = SocketSubscription.CreateForIdentifier(identifier, false, action); foreach (var connection in sockets.Values) - connection.AddHandler(identifier, false, action); + connection.AddHandler(handler); } /// @@ -429,7 +437,11 @@ namespace CryptoExchange.Net var socket = CreateSocket(address); var socketWrapper = new SocketConnection(this, socket); foreach (var kvp in genericHandlers) - socketWrapper.AddHandler(kvp.Key, false, kvp.Value); + { + var handler = SocketSubscription.CreateForIdentifier(kvp.Key, false, kvp.Value); + socketWrapper.AddHandler(handler); + } + return socketWrapper; } @@ -468,7 +480,7 @@ namespace CryptoExchange.Net socket.DataInterpreterString = dataInterpreterString; socket.OnError += e => { - log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e.ToString()); + log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e); }; return socket; } @@ -481,7 +493,7 @@ namespace CryptoExchange.Net public virtual void SendPeriodic(TimeSpan interval, Func objGetter) { if (objGetter == null) - throw new ArgumentNullException("objGetter"); + throw new ArgumentNullException(nameof(objGetter)); periodicEvent = new AutoResetEvent(false); periodicTask = Task.Run(async () => @@ -526,7 +538,7 @@ namespace CryptoExchange.Net public virtual async Task Unsubscribe(UpdateSubscription subscription) { if (subscription == null) - throw new ArgumentNullException("subscription"); + throw new ArgumentNullException(nameof(subscription)); log.Write(LogVerbosity.Info, "Closing subscription"); await subscription.Close().ConfigureAwait(false); @@ -538,7 +550,7 @@ namespace CryptoExchange.Net /// public virtual async Task UnsubscribeAll() { - log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.handlers.Count(h => h.UserSubscription))} subscriptions"); + log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.HandlerCount)} subscriptions"); await Task.Run(() => { diff --git a/CryptoExchange.Net/Sockets/BaseSocket.cs b/CryptoExchange.Net/Sockets/BaseSocket.cs index e2a9aa5..a69be1f 100644 --- a/CryptoExchange.Net/Sockets/BaseSocket.cs +++ b/CryptoExchange.Net/Sockets/BaseSocket.cs @@ -16,45 +16,111 @@ namespace CryptoExchange.Net.Sockets /// /// Socket implementation /// - internal class BaseSocket: IWebsocket + public class BaseSocket: IWebsocket { internal static int lastStreamId; private static readonly object streamIdLock = new object(); + /// + /// Socket + /// protected WebSocket? socket; + /// + /// Log + /// protected Log log; - protected object socketLock = new object(); + private readonly object socketLock = new object(); + /// + /// Error handlers + /// protected readonly List> errorHandlers = new List>(); + /// + /// Open handlers + /// protected readonly List openHandlers = new List(); + /// + /// Close handlers + /// protected readonly List closeHandlers = new List(); + /// + /// Message handlers + /// protected readonly List> messageHandlers = new List>(); - protected IDictionary cookies; - protected IDictionary headers; - protected HttpConnectProxy? proxy; + private readonly IDictionary cookies; + private readonly IDictionary headers; + private HttpConnectProxy? proxy; + /// + /// Id + /// public int Id { get; } + /// + /// If is reconnecting + /// public bool Reconnecting { get; set; } + /// + /// Origin + /// public string? Origin { get; set; } + /// + /// Url + /// public string Url { get; } - public bool IsClosed => socket?.State == null ? true: socket.State == WebSocketState.Closed; + /// + /// Is closed + /// + public bool IsClosed => socket?.State == null || socket.State == WebSocketState.Closed; + /// + /// Is open + /// public bool IsOpen => socket?.State == WebSocketState.Open; + /// + /// Protocols + /// public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; + /// + /// Interpreter for bytes + /// public Func? DataInterpreterBytes { get; set; } + /// + /// Interpreter for strings + /// public Func? DataInterpreterString { get; set; } + /// + /// Last action time + /// public DateTime LastActionTime { get; private set; } + /// + /// Timeout + /// public TimeSpan Timeout { get; set; } private Task? timeoutTask; + /// + /// Socket state + /// public WebSocketState SocketState => socket?.State ?? WebSocketState.None; + /// + /// ctor + /// + /// + /// public BaseSocket(Log log, string url):this(log, url, new Dictionary(), new Dictionary()) { } + /// + /// ctor + /// + /// + /// + /// + /// public BaseSocket(Log log, string url, IDictionary cookies, IDictionary headers) { Id = NextStreamId(); @@ -94,27 +160,43 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// On close + /// public event Action OnClose { add => closeHandlers.Add(value); remove => closeHandlers.Remove(value); } + /// + /// On message + /// public event Action OnMessage { add => messageHandlers.Add(value); remove => messageHandlers.Remove(value); } + /// + /// On error + /// public event Action OnError { add => errorHandlers.Add(value); remove => errorHandlers.Remove(value); } + /// + /// On open + /// public event Action OnOpen { add => openHandlers.Add(value); remove => openHandlers.Remove(value); } + /// + /// Handle + /// + /// protected void Handle(List handlers) { LastActionTime = DateTime.UtcNow; @@ -122,6 +204,12 @@ namespace CryptoExchange.Net.Sockets handle?.Invoke(); } + /// + /// Handle + /// + /// + /// + /// protected void Handle(List> handlers, T data) { LastActionTime = DateTime.UtcNow; @@ -129,6 +217,10 @@ namespace CryptoExchange.Net.Sockets handle?.Invoke(data); } + /// + /// Checks if timed out + /// + /// protected async Task CheckTimeout() { while (true) @@ -150,6 +242,10 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Close socket + /// + /// public virtual async Task Close() { await Task.Run(() => @@ -184,6 +280,9 @@ namespace CryptoExchange.Net.Sockets }).ConfigureAwait(false); } + /// + /// Reset socket + /// public virtual void Reset() { lock (socketLock) @@ -194,11 +293,19 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Send data + /// + /// public virtual void Send(string data) { socket?.Send(data); } + /// + /// Connect socket + /// + /// public virtual Task Connect() { if (socket == null) @@ -259,7 +366,9 @@ namespace CryptoExchange.Net.Sockets timeoutTask = Task.Run(CheckTimeout); } else + { log?.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State); + } } if (socket.State == WebSocketState.Connecting) @@ -269,6 +378,11 @@ namespace CryptoExchange.Net.Sockets }); } + /// + /// Set a proxy + /// + /// + /// public virtual void SetProxy(string host, int port) { proxy = IPAddress.TryParse(host, out var address) @@ -276,6 +390,9 @@ namespace CryptoExchange.Net.Sockets : new HttpConnectProxy(new DnsEndPoint(host, port)); } + /// + /// Dispose + /// public void Dispose() { lock (socketLock) diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 4c8443b..b7e8b9a 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -65,7 +65,7 @@ namespace CryptoExchange.Net.Sockets /// public bool PausedActivity { get; set; } - internal readonly List handlers; + private readonly List handlers; private readonly object handlersLock = new object(); private bool lostTriggered; @@ -109,38 +109,7 @@ namespace CryptoExchange.Net.Sockets Connected = true; }; } - - /// - /// Add a handler - /// - /// The request object - /// If it is a user subscription or a generic handler - /// The data handler - /// - public SocketSubscription AddHandler(object request, bool userSubscription, Action dataHandler) - { - var handler = new SocketSubscription(request, userSubscription, dataHandler); - lock (handlersLock) - handlers.Add(handler); - return handler; - } - - /// - /// Add a handler - /// - /// The identifier of the handler - /// If it is a user subscription or a generic handler - /// The data handler - /// - /// - public SocketSubscription AddHandler(string identifier, bool userSubscription, Action dataHandler) - { - var handler = new SocketSubscription(identifier, userSubscription, dataHandler); - lock (handlersLock) - handlers.Add(handler); - return handler; - } - + private void ProcessMessage(string data) { log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data); @@ -167,6 +136,16 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Add handler + /// + /// + public void AddHandler(SocketSubscription handler) + { + lock(handlersLock) + handlers.Add(handler); + } + private bool HandleData(JToken tokenData) { SocketSubscription? currentSubscription = null; diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index 0f1df22..492ab9d 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -36,30 +36,38 @@ namespace CryptoExchange.Net.Sockets /// public bool Confirmed { get; set; } - /// - /// ctor - /// - /// - /// - /// - public SocketSubscription(object request, bool userSubscription, Action dataHandler) + private SocketSubscription(object? request, string? identifier, bool userSubscription, Action dataHandler) { UserSubscription = userSubscription; MessageHandler = dataHandler; Request = request; + Identifier = identifier; } /// - /// ctor + /// Create SocketSubscription for a request + /// + /// + /// + /// + /// + public static SocketSubscription CreateForRequest(object request, bool userSubscription, + Action dataHandler) + { + return new SocketSubscription(request, null, userSubscription, dataHandler); + } + + /// + /// Create SocketSubscription for an identifier /// /// /// /// - public SocketSubscription(string identifier, bool userSubscription, Action dataHandler) + /// + public static SocketSubscription CreateForIdentifier(string identifier, bool userSubscription, + Action dataHandler) { - UserSubscription = userSubscription; - MessageHandler = dataHandler; - Identifier = identifier; + return new SocketSubscription(null, identifier, userSubscription, dataHandler); } /// From 0699d91b04b69acc2271a40297ed9a57e5a2df2d Mon Sep 17 00:00:00 2001 From: JKorf Date: Mon, 21 Oct 2019 16:36:31 +0200 Subject: [PATCH 13/41] Added validation methods --- CryptoExchange.Net/CryptoExchange.Net.xml | 174 ++++++++++++++++++++++ CryptoExchange.Net/ExtensionMethods.cs | 50 +++++++ 2 files changed, 224 insertions(+) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index d71e803..e8cdf81 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -507,6 +507,37 @@ + + + Validates an int is one of the allowed values + + Value of the int + Name of the parameter + Allowed values + + + + Validates an int is between two values + + The value of the int + Name of the parameter + Min value + Max value + + + + Validates a string is not null or empty + + The value of the string + Name of the parameter + + + + Validates an object is not null + + The value of the object + Name of the parameter + Rate limiter interface @@ -2820,5 +2851,148 @@ + + + Specifies that is allowed as an input even if the + corresponding type disallows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that is disallowed as an input even if the + corresponding type allows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that a method that will never return under any circumstance. + + + + + Initializes a new instance of the class. + + + + + Specifies that the method will not return if the associated + parameter is passed the specified value. + + + + + Gets the condition parameter value. + Code after the method is considered unreachable by diagnostics if the argument + to the associated parameter matches this value. + + + + + Initializes a new instance of the + class with the specified parameter value. + + + The condition parameter value. + Code after the method is considered unreachable by diagnostics if the argument + to the associated parameter matches this value. + + + + + Specifies that an output may be even if the + corresponding type disallows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that when a method returns , + the parameter may be even if the corresponding type disallows it. + + + + + Gets the return value condition. + If the method returns this value, the associated parameter may be . + + + + + Initializes the attribute with the specified return value condition. + + + The return value condition. + If the method returns this value, the associated parameter may be . + + + + + Specifies that an output is not even if the + corresponding type allows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that the output will be non- if the + named parameter is non-. + + + + + Gets the associated parameter name. + The output will be non- if the argument to the + parameter specified is non-. + + + + + Initializes the attribute with the associated parameter name. + + + The associated parameter name. + The output will be non- if the argument to the + parameter specified is non-. + + + + + Specifies that when a method returns , + the parameter will not be even if the corresponding type allows it. + + + + + Gets the return value condition. + If the method returns this value, the associated parameter will not be . + + + + + Initializes the attribute with the specified return value condition. + + + The return value condition. + If the method returns this value, the associated parameter will not be . + + diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 116a904..9fd7e1c 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -213,5 +213,55 @@ namespace CryptoExchange.Net return null; } } + + /// + /// Validates an int is one of the allowed values + /// + /// Value of the int + /// Name of the parameter + /// Allowed values + public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues) + { + if (!allowedValues.Contains(value)) + throw new ArgumentException( + $"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}"); + } + + /// + /// Validates an int is between two values + /// + /// The value of the int + /// Name of the parameter + /// Min value + /// Max value + public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue) + { + if (value < minValue || value > maxValue) + throw new ArgumentException( + $"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}"); + } + + /// + /// Validates a string is not null or empty + /// + /// The value of the string + /// Name of the parameter + public static void ValidateNotNull(this string value, string argumentName) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException($"No value provided for parameter {argumentName}"); + } + + /// + /// Validates an object is not null + /// + /// The value of the object + /// Name of the parameter + public static void ValidateNotNull(this object value, string argumentName) + { + if (value == null) + throw new ArgumentException($"No value provided for parameter {argumentName}"); + } } } + From 3867a59286b1fd9e2bdc79696b9ba5c59277fc31 Mon Sep 17 00:00:00 2001 From: JKorf Date: Mon, 21 Oct 2019 16:42:11 +0200 Subject: [PATCH 14/41] Updated travis config --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 100393f..e2b1f4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: csharp mono: none solution: CryptoExchange.Net.sln -dotnet: 2.0.0 -dist: xenial +dotnet: 3.0 script: - - dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.0" + - dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.1" - dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj \ No newline at end of file From 964403d750fddf0dadb7c20498791b06c46c8de2 Mon Sep 17 00:00:00 2001 From: JKorf Date: Mon, 21 Oct 2019 16:47:41 +0200 Subject: [PATCH 15/41] Unit test project to .netcore3.0 --- .../CryptoExchange.Net.UnitTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj index f521b29..ed4890c 100644 --- a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp3.0 false From 3ba0507a795fb3521a03f16f4cb13f376b75d542 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 21 Oct 2019 21:44:51 +0200 Subject: [PATCH 16/41] Cleaned up order book --- CryptoExchange.Net/CryptoExchange.Net.xml | 66 +++++-------------- CryptoExchange.Net/ExtensionMethods.cs | 11 ++++ .../Interfaces/ISymbolOrderBook.cs | 2 +- .../OrderBook/OrderBookEntry.cs | 30 --------- .../OrderBook/ProcessBufferEntry.cs | 15 ++--- CryptoExchange.Net/OrderBook/ProcessEntry.cs | 31 --------- .../OrderBook/SymbolOrderBook.cs | 37 ++++++----- 7 files changed, 55 insertions(+), 137 deletions(-) delete mode 100644 CryptoExchange.Net/OrderBook/OrderBookEntry.cs delete mode 100644 CryptoExchange.Net/OrderBook/ProcessEntry.cs diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index e8cdf81..ca429f4 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -538,6 +538,13 @@ The value of the object Name of the parameter + + + Validates a list is not null or empty + + The value of the object + Name of the parameter + Rate limiter interface @@ -1637,28 +1644,6 @@ - - - Order book entry - - - - - Quantity of the entry - - - - - Price of the entry - - - - - ctor - - - - Buffer entry for order book @@ -1674,38 +1659,16 @@ The last sequence number of the entries - + - List of entries + List of asks - + - ctor + List of bids - - - Process entry for order book - - - - - The entry - - - - - The type - - - - - ctor - - - - Base for order book implementations @@ -1758,7 +1721,7 @@ - Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets @@ -1852,13 +1815,14 @@ List of asks List of bids - + Update the order book with entries First sequence number Last sequence number - List of entries + List of bids + List of asks diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 9fd7e1c..1090cf9 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -262,6 +262,17 @@ namespace CryptoExchange.Net if (value == null) throw new ArgumentException($"No value provided for parameter {argumentName}"); } + + /// + /// Validates a list is not null or empty + /// + /// The value of the object + /// Name of the parameter + public static void ValidateNotNull(this IEnumerable value, string argumentName) + { + if (value == null || !value.Any()) + throw new ArgumentException($"No values provided for parameter {argumentName}"); + } } } diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index ad09904..1571feb 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets /// - event Action OnOrderBookUpdate; + event Action, IEnumerable> OnOrderBookUpdate; /// /// Timestamp of the last update /// diff --git a/CryptoExchange.Net/OrderBook/OrderBookEntry.cs b/CryptoExchange.Net/OrderBook/OrderBookEntry.cs deleted file mode 100644 index 4bf9c39..0000000 --- a/CryptoExchange.Net/OrderBook/OrderBookEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using CryptoExchange.Net.Interfaces; - -namespace CryptoExchange.Net.OrderBook -{ - /// - /// Order book entry - /// - public class OrderBookEntry : ISymbolOrderBookEntry - { - /// - /// Quantity of the entry - /// - public decimal Quantity { get; set; } - /// - /// Price of the entry - /// - public decimal Price { get; set; } - - /// - /// ctor - /// - /// - /// - public OrderBookEntry(decimal price, decimal quantity) - { - Quantity = quantity; - Price = price; - } - } -} diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 8a48794..397f53a 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using CryptoExchange.Net.Interfaces; +using System.Collections.Generic; namespace CryptoExchange.Net.OrderBook { @@ -16,16 +17,12 @@ namespace CryptoExchange.Net.OrderBook /// public long LastSequence { get; set; } /// - /// List of entries + /// List of asks /// - public List Entries { get; set; } - + public IEnumerable Asks { get; set; } = new List(); /// - /// ctor + /// List of bids /// - public ProcessBufferEntry() - { - Entries = new List(); - } + public IEnumerable Bids { get; set; } = new List(); } } diff --git a/CryptoExchange.Net/OrderBook/ProcessEntry.cs b/CryptoExchange.Net/OrderBook/ProcessEntry.cs deleted file mode 100644 index 5ebff10..0000000 --- a/CryptoExchange.Net/OrderBook/ProcessEntry.cs +++ /dev/null @@ -1,31 +0,0 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.OrderBook -{ - /// - /// Process entry for order book - /// - public class ProcessEntry - { - /// - /// The entry - /// - public ISymbolOrderBookEntry Entry { get; set; } - /// - /// The type - /// - public OrderBookEntryType Type { get; set; } - - /// - /// ctor - /// - /// - /// - public ProcessEntry(OrderBookEntryType type, ISymbolOrderBookEntry entry) - { - Type = type; - Entry = entry; - } - } -} diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 675e8e8..4597e7d 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -77,9 +77,9 @@ namespace CryptoExchange.Net.OrderBook /// public event Action? OnStatusChange; /// - /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + /// Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets /// - public event Action? OnOrderBookUpdate; + public event Action, IEnumerable>? OnOrderBookUpdate; /// /// Timestamp of the last update /// @@ -268,10 +268,10 @@ namespace CryptoExchange.Net.OrderBook asks.Clear(); foreach (var ask in askList) - asks.Add(ask.Price, new OrderBookEntry(ask.Price, ask.Quantity)); + asks.Add(ask.Price, ask); bids.Clear(); foreach (var bid in bidList) - bids.Add(bid.Price, new OrderBookEntry(bid.Price, bid.Quantity)); + bids.Add(bid.Price, bid); LastSequenceNumber = orderBookSequenceNumber; @@ -281,7 +281,7 @@ namespace CryptoExchange.Net.OrderBook CheckProcessBuffer(); bookSet = true; LastOrderBookUpdate = DateTime.UtcNow; - OnOrderBookUpdate?.Invoke(); + OnOrderBookUpdate?.Invoke(bidList, askList); log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } @@ -291,8 +291,9 @@ namespace CryptoExchange.Net.OrderBook /// /// First sequence number /// Last sequence number - /// List of entries - protected void UpdateOrderBook(long firstSequenceNumber, long lastSequenceNumber, List entries) + /// List of bids + /// List of asks + protected void UpdateOrderBook(long firstSequenceNumber, long lastSequenceNumber, IEnumerable bids, IEnumerable asks) { lock (bookLock) { @@ -305,7 +306,8 @@ namespace CryptoExchange.Net.OrderBook { FirstSequence = firstSequenceNumber, LastSequence = lastSequenceNumber, - Entries = entries + Asks = asks, + Bids = bids }; processBuffer.Add(entry); log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update before synced; buffering"); @@ -318,13 +320,15 @@ namespace CryptoExchange.Net.OrderBook } else { - foreach (var entry in entries) - ProcessUpdate(entry.Type, entry.Entry); + foreach (var entry in asks) + ProcessUpdate(OrderBookEntryType.Ask, entry); + foreach (var entry in bids) + ProcessUpdate(OrderBookEntryType.Bid, entry); LastSequenceNumber = lastSequenceNumber; CheckProcessBuffer(); LastOrderBookUpdate = DateTime.UtcNow; - OnOrderBookUpdate?.Invoke(); - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update: {entries.Count} entries processed"); + OnOrderBookUpdate?.Invoke(bids, asks); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update: {asks.Count()} asks, {bids.Count()} bids processed"); } } } @@ -345,8 +349,11 @@ namespace CryptoExchange.Net.OrderBook if (bufferEntry.FirstSequence > LastSequenceNumber + 1) break; - foreach (var entry in bufferEntry.Entries) - ProcessUpdate(entry.Type, entry.Entry); + foreach (var entry in bufferEntry.Asks) + ProcessUpdate(OrderBookEntryType.Ask, entry); + foreach (var entry in bufferEntry.Bids) + ProcessUpdate(OrderBookEntryType.Bid, entry); + processBuffer.Remove(bufferEntry); LastSequenceNumber = bufferEntry.LastSequence; } @@ -373,7 +380,7 @@ namespace CryptoExchange.Net.OrderBook { if (!listToChange.ContainsKey(entry.Price)) { - listToChange.Add(entry.Price, new OrderBookEntry(entry.Price, entry.Quantity)); + listToChange.Add(entry.Price, entry); if (type == OrderBookEntryType.Ask) AskCount++; else BidCount++; } From 6f75af507ae2a0d4628f77ffe1770926c59c4d80 Mon Sep 17 00:00:00 2001 From: JKorf Date: Tue, 22 Oct 2019 16:29:08 +0200 Subject: [PATCH 17/41] Renames, orderbook changes --- CryptoExchange.Net/CryptoExchange.Net.xml | 236 ++++++----------- .../Interfaces/ISymbolOrderBookEntry.cs | 11 + .../OrderBook/ProcessBufferEntry.cs | 36 ++- .../OrderBook/SymbolOrderBook.cs | 240 ++++++++++++++---- 4 files changed, 310 insertions(+), 213 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index ca429f4..45fcd78 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -871,6 +871,16 @@ The price of the entry + + + Interface for order book entries + + + + + Sequence of the update + + Interface for websocket interaction @@ -1649,16 +1659,6 @@ Buffer entry for order book - - - The first sequence number of the entries - - - - - The last sequence number of the entries - - List of asks @@ -1669,6 +1669,41 @@ List of bids + + + First update id + + + + + List of asks + + + + + List of bids + + + + + First update id + + + + + Last update id + + + + + List of asks + + + + + List of bids + + Base for order book implementations @@ -1699,6 +1734,11 @@ The log + + + If order book is set + + The status of the order book. Order book is up to date when the status is `Synced` @@ -1722,7 +1762,7 @@ Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets - + @@ -1815,12 +1855,27 @@ List of asks List of bids + + + Update the order book using a single id for an update + + + + + - Update the order book with entries + Update the order book using a first/last update id + + + + + + + + + Update the order book using sequenced entries - First sequence number - Last sequence number List of bids List of asks @@ -1829,13 +1884,21 @@ Check and empty the process buffer; see what entries to update the book with - + Update order book with an entry + Sequence number of the update Type of entry The entry + + + Wait until the order book has been set + + Max wait time + + Dispose the order book @@ -2815,148 +2878,5 @@ - - - Specifies that is allowed as an input even if the - corresponding type disallows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that is disallowed as an input even if the - corresponding type allows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that a method that will never return under any circumstance. - - - - - Initializes a new instance of the class. - - - - - Specifies that the method will not return if the associated - parameter is passed the specified value. - - - - - Gets the condition parameter value. - Code after the method is considered unreachable by diagnostics if the argument - to the associated parameter matches this value. - - - - - Initializes a new instance of the - class with the specified parameter value. - - - The condition parameter value. - Code after the method is considered unreachable by diagnostics if the argument - to the associated parameter matches this value. - - - - - Specifies that an output may be even if the - corresponding type disallows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that when a method returns , - the parameter may be even if the corresponding type disallows it. - - - - - Gets the return value condition. - If the method returns this value, the associated parameter may be . - - - - - Initializes the attribute with the specified return value condition. - - - The return value condition. - If the method returns this value, the associated parameter may be . - - - - - Specifies that an output is not even if the - corresponding type allows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that the output will be non- if the - named parameter is non-. - - - - - Gets the associated parameter name. - The output will be non- if the argument to the - parameter specified is non-. - - - - - Initializes the attribute with the associated parameter name. - - - The associated parameter name. - The output will be non- if the argument to the - parameter specified is non-. - - - - - Specifies that when a method returns , - the parameter will not be even if the corresponding type allows it. - - - - - Gets the return value condition. - If the method returns this value, the associated parameter will not be . - - - - - Initializes the attribute with the specified return value condition. - - - The return value condition. - If the method returns this value, the associated parameter will not be . - - diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs index 419f41a..3aa9f97 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs @@ -14,4 +14,15 @@ /// decimal Price { get; set; } } + + /// + /// Interface for order book entries + /// + public interface ISymbolOrderSequencedBookEntry: ISymbolOrderBookEntry + { + /// + /// Sequence of the update + /// + long Sequence { get; set; } + } } diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 397f53a..b5ebfe9 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -9,13 +9,41 @@ namespace CryptoExchange.Net.OrderBook public class ProcessBufferEntry { /// - /// The first sequence number of the entries + /// List of asks /// - public long FirstSequence { get; set; } + public IEnumerable Asks { get; set; } = new List(); /// - /// The last sequence number of the entries + /// List of bids /// - public long LastSequence { get; set; } + public IEnumerable Bids { get; set; } = new List(); + } + + public class ProcessBufferSingleSequenceEntry + { + /// + /// First update id + /// + public long UpdateId { get; set; } + /// + /// List of asks + /// + public IEnumerable Asks { get; set; } = new List(); + /// + /// List of bids + /// + public IEnumerable Bids { get; set; } = new List(); + } + + public class ProcessBufferRangeSequenceEntry + { + /// + /// First update id + /// + public long FirstUpdateId { get; set; } + /// + /// Last update id + /// + public long LastUpdateId { get; set; } /// /// List of asks /// diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 4597e7d..cc890c8 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The process buffer, used while syncing /// - protected readonly List processBuffer; + protected readonly List processBuffer; private readonly object bookLock = new object(); /// /// The ask list @@ -43,7 +43,10 @@ namespace CryptoExchange.Net.OrderBook /// protected Log log; - private bool bookSet; + /// + /// If order book is set + /// + protected bool bookSet; /// /// The status of the order book. Order book is up to date when the status is `Synced` @@ -78,7 +81,7 @@ namespace CryptoExchange.Net.OrderBook public event Action? OnStatusChange; /// /// Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets - /// + /// public event Action, IEnumerable>? OnOrderBookUpdate; /// /// Timestamp of the last update @@ -156,7 +159,7 @@ namespace CryptoExchange.Net.OrderBook throw new ArgumentNullException(nameof(options)); Id = options.OrderBookName; - processBuffer = new List(); + processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; Symbol = symbol; Status = OrderBookStatus.Disconnected; @@ -259,11 +262,11 @@ namespace CryptoExchange.Net.OrderBook /// The last update sequence number /// List of asks /// List of bids - protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable askList, IEnumerable bidList) + protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable bidList, IEnumerable askList) { lock (bookLock) { - if (Status == OrderBookStatus.Connecting) + if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) return; asks.Clear(); @@ -278,99 +281,213 @@ namespace CryptoExchange.Net.OrderBook AskCount = asks.Count; BidCount = asks.Count; - CheckProcessBuffer(); bookSet = true; LastOrderBookUpdate = DateTime.UtcNow; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{orderBookSequenceNumber}"); + CheckProcessBuffer(); OnOrderBookUpdate?.Invoke(bidList, askList); - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); } } /// - /// Update the order book with entries + /// Update the order book using a single id for an update /// - /// First sequence number - /// Last sequence number - /// List of bids - /// List of asks - protected void UpdateOrderBook(long firstSequenceNumber, long lastSequenceNumber, IEnumerable bids, IEnumerable asks) + /// + /// + /// + protected void UpdateOrderBook(long rangeUpdateId, IEnumerable bids, IEnumerable asks) { lock (bookLock) { - if (lastSequenceNumber < LastSequenceNumber) + if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) return; if (!bookSet) { - var entry = new ProcessBufferEntry + processBuffer.Add(new ProcessBufferSingleSequenceEntry() { - FirstSequence = firstSequenceNumber, - LastSequence = lastSequenceNumber, + UpdateId = rangeUpdateId, Asks = asks, Bids = bids - }; - processBuffer.Add(entry); - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update before synced; buffering"); - } - else if (sequencesAreConsecutive && firstSequenceNumber > LastSequenceNumber + 1) - { - // Out of sync - log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync, reconnecting"); - subscription!.Reconnect().Wait(); + }); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{rangeUpdateId}"); } else { - foreach (var entry in asks) - ProcessUpdate(OrderBookEntryType.Ask, entry); - foreach (var entry in bids) - ProcessUpdate(OrderBookEntryType.Bid, entry); - LastSequenceNumber = lastSequenceNumber; CheckProcessBuffer(); - LastOrderBookUpdate = DateTime.UtcNow; + ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update: {asks.Count()} asks, {bids.Count()} bids processed"); } } } + /// + /// Update the order book using a first/last update id + /// + /// + /// + /// + /// + protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + { + lock (bookLock) + { + if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) + return; + + if (!bookSet) + { + processBuffer.Add(new ProcessBufferRangeSequenceEntry() + { + Asks = asks, + Bids = bids, + FirstUpdateId = firstUpdateId, + LastUpdateId = lastUpdateId + }); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{firstUpdateId}-{lastUpdateId}"); + } + else + { + CheckProcessBuffer(); + ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks); + OnOrderBookUpdate?.Invoke(bids, asks); + } + } + } + + /// + /// Update the order book using sequenced entries + /// + /// List of bids + /// List of asks + protected void UpdateOrderBook(IEnumerable bids, IEnumerable asks) + { + lock (bookLock) + { + if (!bookSet) + { + processBuffer.Add(new ProcessBufferEntry + { + Asks = asks, + Bids = bids + }); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{Math.Min(bids.Min(b => b.Sequence), asks.Min(a => a.Sequence))}-{Math.Max(bids.Max(b => b.Sequence), asks.Max(a => a.Sequence))}"); + } + else + { + CheckProcessBuffer(); + ProcessUpdates(bids, asks); + OnOrderBookUpdate?.Invoke(bids, asks); + } + } + } + + private void ProcessUpdates(IEnumerable bids, IEnumerable asks) + { + var entries = new Dictionary(); + foreach (var entry in asks.OrderBy(a => a.Sequence)) + entries.Add(entry, OrderBookEntryType.Ask); + foreach (var entry in bids.OrderBy(a => a.Sequence)) + entries.Add(entry, OrderBookEntryType.Bid); + + foreach (var entry in entries.OrderBy(e => e.Key.Sequence)) + { + if(ProcessUpdate(entry.Key.Sequence, entry.Value, entry.Key)) + LastSequenceNumber = entry.Key.Sequence; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update #{LastSequenceNumber}"); + } + } + + private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + { + if (lastUpdateId < LastSequenceNumber) + { + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}"); + return; + } + + foreach (var entry in bids) + ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry); + + foreach (var entry in asks) + ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry); + + LastSequenceNumber = lastUpdateId; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}"); + } + + private void ProcessSingleSequenceUpdates(long updateId, IEnumerable bids, IEnumerable asks) + { + foreach (var entry in bids) + { + if (!ProcessUpdate(updateId, OrderBookEntryType.Bid, entry)) + return; + } + + foreach (var entry in asks) + { + if (!ProcessUpdate(updateId, OrderBookEntryType.Ask, entry)) + return; + } + + LastSequenceNumber = updateId; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{LastSequenceNumber}"); + } + /// /// Check and empty the process buffer; see what entries to update the book with /// protected void CheckProcessBuffer() { - foreach (var bufferEntry in processBuffer.OrderBy(b => b.FirstSequence).ToList()) + var pbList = processBuffer.ToList(); + if(pbList.Count > 0) + log.Write(LogVerbosity.Debug, "Processing buffered updates"); + + foreach (var bufferEntry in pbList) { - if (bufferEntry.LastSequence < LastSequenceNumber) - { - processBuffer.Remove(bufferEntry); - continue; - } - - if (bufferEntry.FirstSequence > LastSequenceNumber + 1) - break; - - foreach (var entry in bufferEntry.Asks) - ProcessUpdate(OrderBookEntryType.Ask, entry); - foreach (var entry in bufferEntry.Bids) - ProcessUpdate(OrderBookEntryType.Bid, entry); + if (bufferEntry is ProcessBufferEntry pbe) + ProcessUpdates(pbe.Bids, pbe.Asks); + else if(bufferEntry is ProcessBufferRangeSequenceEntry pbrse) + ProcessRangeUpdates(pbrse.FirstUpdateId, pbrse.LastUpdateId, pbrse.Bids, pbrse.Asks); + else if (bufferEntry is ProcessBufferSingleSequenceEntry pbsse) + ProcessSingleSequenceUpdates(pbsse.UpdateId, pbsse.Bids, pbsse.Asks); processBuffer.Remove(bufferEntry); - LastSequenceNumber = bufferEntry.LastSequence; } } /// /// Update order book with an entry /// + /// Sequence number of the update /// Type of entry /// The entry - protected virtual void ProcessUpdate(OrderBookEntryType type, ISymbolOrderBookEntry entry) + protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry) { + if (Status != OrderBookStatus.Syncing && Status != OrderBookStatus.Synced) + return false; + + if (sequence <= LastSequenceNumber) + { + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{sequence}"); + return false; + } + + if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1) + { + // Out of sync + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); + Status = OrderBookStatus.Connecting; + subscription?.Reconnect(); + return false; + } + + LastOrderBookUpdate = DateTime.UtcNow; var listToChange = type == OrderBookEntryType.Ask ? asks : bids; if (entry.Quantity == 0) { if (!listToChange.ContainsKey(entry.Price)) - return; + return true; listToChange.Remove(entry.Price); if (type == OrderBookEntryType.Ask) AskCount--; @@ -389,6 +506,27 @@ namespace CryptoExchange.Net.OrderBook listToChange[entry.Price].Quantity = entry.Quantity; } } + + return true; + } + + /// + /// Wait until the order book has been set + /// + /// Max wait time + /// + protected async Task> WaitForSetOrderBook(int timeout) + { + var startWait = DateTime.UtcNow; + while (!bookSet && Status == OrderBookStatus.Syncing) + { + if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout) + return new CallResult(false, new ServerError("Timeout while waiting for data")); + + await Task.Delay(10).ConfigureAwait(false); + } + + return new CallResult(true, null); } /// From bfdc83ffe5e028c9396c5a11ce1e7485ba74ae12 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Tue, 22 Oct 2019 22:43:53 +0200 Subject: [PATCH 18/41] Comments --- CryptoExchange.Net/CryptoExchange.Net.xml | 10 ++++++++++ CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 45fcd78..c19f104 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -1669,6 +1669,11 @@ List of bids + + + Buffer entry with a single update id per update + + First update id @@ -1684,6 +1689,11 @@ List of bids + + + Buffer entry with a first and last update id + + First update id diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index b5ebfe9..3cf64bc 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -18,6 +18,9 @@ namespace CryptoExchange.Net.OrderBook public IEnumerable Bids { get; set; } = new List(); } + /// + /// Buffer entry with a single update id per update + /// public class ProcessBufferSingleSequenceEntry { /// @@ -34,6 +37,9 @@ namespace CryptoExchange.Net.OrderBook public IEnumerable Bids { get; set; } = new List(); } + /// + /// Buffer entry with a first and last update id + /// public class ProcessBufferRangeSequenceEntry { /// From 5bfabc34c237301bd946d889a3299dd20ccd6fc2 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 08:48:12 +0200 Subject: [PATCH 19/41] cleanup --- CryptoExchange.Net.UnitTests/RestClientTests.cs | 7 +++---- CryptoExchange.Net.UnitTests/SocketClientTests.cs | 2 -- .../TestImplementations/TestHelpers.cs | 4 +--- .../TestImplementations/TestObjects.cs | 3 --- .../TestImplementations/TestRestClient.cs | 2 -- .../TestImplementations/TestSocket.cs | 2 -- CryptoExchange.Net/Attributes/NullableAttributes.cs | 2 +- CryptoExchange.Net/Converters/BaseConverter.cs | 1 - CryptoExchange.Net/Interfaces/IRequest.cs | 2 -- CryptoExchange.Net/Requests/Request.cs | 1 - CryptoExchange.Net/RestClient.cs | 4 ++-- 11 files changed, 7 insertions(+), 23 deletions(-) diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 963e4f4..d07c5d9 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -7,10 +7,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; -using System.Net.Http; -using System.Text; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.RateLimiter; @@ -110,7 +107,8 @@ namespace CryptoExchange.Net.UnitTests { BaseAddress = "http://test.address.com", RateLimiters = new List{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))}, - RateLimitingBehaviour = RateLimitingBehaviour.Fail + RateLimitingBehaviour = RateLimitingBehaviour.Fail, + RequestTimeout = TimeSpan.FromMinutes(1) }); @@ -118,6 +116,7 @@ namespace CryptoExchange.Net.UnitTests Assert.IsTrue(client.BaseAddress == "http://test.address.com"); Assert.IsTrue(client.RateLimiters.Count() == 1); Assert.IsTrue(client.RateLimitBehaviour == RateLimitingBehaviour.Fail); + Assert.IsTrue(client.RequestTimeout == TimeSpan.FromMinutes(1)); } [TestCase] diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index e7d4049..cef45f4 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs index b856d0f..9e95043 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Text; namespace CryptoExchange.Net.UnitTests.TestImplementations { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs index 7a6f2cc..0ad099d 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs @@ -1,7 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.UnitTests.TestImplementations { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 8364895..870c833 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -3,11 +3,9 @@ using CryptoExchange.Net.Objects; using Moq; using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Threading; diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs index 5430a71..ef757e9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Security.Authentication; -using System.Text; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; using WebSocket4Net; diff --git a/CryptoExchange.Net/Attributes/NullableAttributes.cs b/CryptoExchange.Net/Attributes/NullableAttributes.cs index cb75272..11267ea 100644 --- a/CryptoExchange.Net/Attributes/NullableAttributes.cs +++ b/CryptoExchange.Net/Attributes/NullableAttributes.cs @@ -1,7 +1,7 @@ #if !NETSTANDARD2_1 namespace System.Diagnostics.CodeAnalysis { - using global::System; + using System; /// /// Specifies that is allowed as an input even if the diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index da8e3d5..b21a521 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Newtonsoft.Json; diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index c8b9403..9e819cb 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 3387cde..5c4530c 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 11aa854..efe3e21 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -47,13 +47,13 @@ namespace CryptoExchange.Net /// /// Timeout for requests /// - protected TimeSpan RequestTimeout { get; } + public TimeSpan RequestTimeout { get; } /// /// Rate limiting behaviour /// public RateLimitingBehaviour RateLimitBehaviour { get; } /// - /// List of ratelimitters + /// List of rate limiters /// public IEnumerable RateLimiters { get; private set; } /// From 4e95782fa60907e53c6b4a004697097e06e7a727 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 09:01:58 +0200 Subject: [PATCH 20/41] First readme update --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index efb84c8..980f31b 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ To unsubscribe use the client.Unsubscribe method and pass the UpdateSubscription ````C# // Subscribe var client = new BinanceSocketClient(); -var subResult = client.SubscribeToDepthStream("BTCUSDT", data => {}); +var subResult = client.SubscribeToOrderBookUpdates("BTCUSDT", data => {}); // Unsubscribe client.Unsubscribe(subResult.Data); @@ -172,6 +172,7 @@ but the implementation is similar for each library: ````C# var orderBook = new BinanceSymbolOrderBook("BTCUSDT", new BinanceOrderBookOptions(20)); orderBook.OnStatusChange += (oldStatus, newStatus) => Console.WriteLine($"Book state changed from {oldStatus} to {newStatus}"); +orderBook.OnOrderBookUpdate += (changedBids, changedAsks) => Console.WriteLine("Book updated"); var startResult = await orderBook.StartAsync(); if(!startResult.Success) { From d253858b147b42dadc5242a6b4f850c7307cd035 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 09:34:15 +0200 Subject: [PATCH 21/41] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 980f31b..be9995f 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,16 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 3.0.0 - 23 Oct 2019 + * Updated to C# 8.0 + * Added .NetStandard2.1 support + * Added Nullability support + * Now using HttpClient instead of WebRequest, should result in faster consequtive requests + * Added CancellationToken support + * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`) + * Added input validation methods + * OnOrderBookUpdate event added to `SymbolOrderBook` + * Version 2.1.8 - 29 Aug 2019 * Added array serialization options for implementations From 169c994a29f9ab3f81bfb2a39b32d0a4c3f40585 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 09:39:35 +0200 Subject: [PATCH 22/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 99af2ea..6c79f27 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -5,12 +5,19 @@ CryptoExchange.Net JKorf - 2.1.8 + 3.0.0 false https://github.com/JKorf/CryptoExchange.Net en true - 2.1.8 - Added array serialization options for implementations + 3.0.0 - * Updated to C# 8.0\n + * Added .NetStandard2.1 support\n + * Added Nullability support\n + * Now using HttpClient instead of WebRequest, should result in faster consequtive requests\n + * Added CancellationToken support\n + * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`)\n + * Added input validation methods\n + * OnOrderBookUpdate event added to `SymbolOrderBook` enable 8.0 MIT From b030b0b4c3de416ed17faaab24980678bd1a45ef Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 09:58:54 +0200 Subject: [PATCH 23/41] Removed excess \n chars from release notes --- CryptoExchange.Net/CryptoExchange.Net.csproj | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 6c79f27..7bcbe22 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -10,13 +10,14 @@ https://github.com/JKorf/CryptoExchange.Net en true - 3.0.0 - * Updated to C# 8.0\n - * Added .NetStandard2.1 support\n - * Added Nullability support\n - * Now using HttpClient instead of WebRequest, should result in faster consequtive requests\n - * Added CancellationToken support\n - * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`)\n - * Added input validation methods\n + 3.0.0 - + * Updated to C# 8.0 + * Added .NetStandard2.1 support + * Added Nullability support + * Now using HttpClient instead of WebRequest, should result in faster consequtive requests + * Added CancellationToken support + * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`) + * Added input validation methods * OnOrderBookUpdate event added to `SymbolOrderBook` enable 8.0 From 539b92b2e89bca221c7f25d5deb91179e7df1e23 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 10:22:33 +0200 Subject: [PATCH 24/41] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index be9995f..a32c033 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,10 @@ To stop synchronizing an order book use the `Stop` method. * Added CancellationToken support * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`) * Added input validation methods + * Wrong input will now throw exceptions rather than error results * OnOrderBookUpdate event added to `SymbolOrderBook` + * Version 2.1.8 - 29 Aug 2019 * Added array serialization options for implementations From dec63e163607c40287202216d0144b91db6eedca Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 23 Oct 2019 15:35:58 +0200 Subject: [PATCH 25/41] Fixed reference icons --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a32c033..96ccade 100644 --- a/README.md +++ b/README.md @@ -14,31 +14,31 @@ A base library for easy implementation of cryptocurrency API's. Include: ## Implementations - - - - - - - From d274a691c3c31fc851d88c87e0cf82f39228bd25 Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Wed, 23 Oct 2019 23:38:43 +0200 Subject: [PATCH 26/41] Re-added debug response messages --- CryptoExchange.Net/BaseClient.cs | 7 +++++++ CryptoExchange.Net/CryptoExchange.Net.xml | 2 +- CryptoExchange.Net/RestClient.cs | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index aa475b6..8c32f2b 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -209,6 +209,13 @@ namespace CryptoExchange.Net try { using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); + if (log.Level == LogVerbosity.Debug) + { + var data = await reader.ReadToEndAsync().ConfigureAwait(false); + log.Write(LogVerbosity.Debug, $"Data received: {data}"); + return new CallResult(JsonConvert.DeserializeObject(data), null); + } + using var jsonReader = new JsonTextReader(reader); return new CallResult(serializer.Deserialize(jsonReader), null); } diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index c19f104..800ba58 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -2111,7 +2111,7 @@ - List of ratelimitters + List of rate limiters diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index efe3e21..521f9b9 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -209,7 +209,6 @@ namespace CryptoExchange.Net responseStream.Close(); response.Close(); - return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error); } else From 93ee180bd1c319996a63d9f5427298dd4de97728 Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 24 Oct 2019 08:43:50 +0200 Subject: [PATCH 27/41] Removed restrictions on code doc generation --- CryptoExchange.Net/CryptoExchange.Net.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 7bcbe22..7966b3b 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -23,7 +23,7 @@ 8.0 MIT - + CryptoExchange.Net.xml From d81b59ffbdc6ad6669614aeaf0747c6915fa8640 Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 14 Nov 2019 11:56:37 +0100 Subject: [PATCH 28/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 18 +++++------------- README.md | 3 +++ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 7966b3b..274e8e0 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,27 +1,19 @@ netstandard2.0;netstandard2.1 - + CryptoExchange.Net JKorf - 3.0.0 + 3.0.1 false https://github.com/JKorf/CryptoExchange.Net en true - 3.0.0 - - * Updated to C# 8.0 - * Added .NetStandard2.1 support - * Added Nullability support - * Now using HttpClient instead of WebRequest, should result in faster consequtive requests - * Added CancellationToken support - * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`) - * Added input validation methods - * OnOrderBookUpdate event added to `SymbolOrderBook` + 3.0.1 - Re-enabled debug response logging enable - 8.0 - MIT + 8.0 + MIT CryptoExchange.Net.xml diff --git a/README.md b/README.md index 96ccade..e56dd18 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,9 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 3.0.1 - 14 Nov 2019 + * Re-enabled debug response logging + * Version 3.0.0 - 23 Oct 2019 * Updated to C# 8.0 * Added .NetStandard2.1 support From 69a618b508214f4fe8b17a26842b120263ba1ff8 Mon Sep 17 00:00:00 2001 From: JKorf Date: Tue, 10 Dec 2019 10:19:03 +0100 Subject: [PATCH 29/41] Removed invalid check preventing setting a unauthorized proxy --- CryptoExchange.Net/Objects/ApiProxy.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/CryptoExchange.Net/Objects/ApiProxy.cs b/CryptoExchange.Net/Objects/ApiProxy.cs index 9b66e7f..bbbd65e 100644 --- a/CryptoExchange.Net/Objects/ApiProxy.cs +++ b/CryptoExchange.Net/Objects/ApiProxy.cs @@ -58,9 +58,6 @@ namespace CryptoExchange.Net.Objects /// The proxy password public ApiProxy(string host, int port, string? login, SecureString? password) { - if (string.IsNullOrEmpty(login)) - throw new ArgumentException("Proxy login not provided"); - if (!host.StartsWith("http")) throw new ArgumentException("Proxy host should start with either http:// or https://"); From a5257095624cdedbebb580d66ce3c790c59e11cf Mon Sep 17 00:00:00 2001 From: JKorf Date: Tue, 10 Dec 2019 10:20:33 +0100 Subject: [PATCH 30/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 4 ++-- README.md | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 274e8e0..b772e21 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -5,12 +5,12 @@ CryptoExchange.Net JKorf - 3.0.1 + 3.0.2 false https://github.com/JKorf/CryptoExchange.Net en true - 3.0.1 - Re-enabled debug response logging + 3.0.2 - Removed invalid check for unauthenticated proxy enable 8.0 MIT diff --git a/README.md b/README.md index e56dd18..970fd4b 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,9 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 3.0.2 - 10 Dec 2019 + * Removed invalid check for unauthenticated proxy + * Version 3.0.1 - 14 Nov 2019 * Re-enabled debug response logging From e7ec10e98d58da16c0f12f334884840bd233f989 Mon Sep 17 00:00:00 2001 From: Ben Davison Date: Wed, 11 Dec 2019 13:26:13 +0000 Subject: [PATCH 31/41] Ticking price event --- CryptoExchange.Net/CryptoExchange.Net.xml | 153 ++++++++++++++++++ .../Interfaces/ISymbolOrderBook.cs | 4 + .../OrderBook/SymbolOrderBook.cs | 32 +++- 3 files changed, 186 insertions(+), 3 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 800ba58..eb9478e 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -797,6 +797,11 @@ Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + + + Event when the BestBid or BestAsk changes ie a Pricing Tick + + Timestamp of the last update @@ -1769,6 +1774,11 @@ Event when the state changes + + + Event when the BestBid or BestAsk changes ie a Pricing Tick + + Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets @@ -2888,5 +2898,148 @@ + + + Specifies that is allowed as an input even if the + corresponding type disallows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that is disallowed as an input even if the + corresponding type allows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that a method that will never return under any circumstance. + + + + + Initializes a new instance of the class. + + + + + Specifies that the method will not return if the associated + parameter is passed the specified value. + + + + + Gets the condition parameter value. + Code after the method is considered unreachable by diagnostics if the argument + to the associated parameter matches this value. + + + + + Initializes a new instance of the + class with the specified parameter value. + + + The condition parameter value. + Code after the method is considered unreachable by diagnostics if the argument + to the associated parameter matches this value. + + + + + Specifies that an output may be even if the + corresponding type disallows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that when a method returns , + the parameter may be even if the corresponding type disallows it. + + + + + Gets the return value condition. + If the method returns this value, the associated parameter may be . + + + + + Initializes the attribute with the specified return value condition. + + + The return value condition. + If the method returns this value, the associated parameter may be . + + + + + Specifies that an output is not even if the + corresponding type allows it. + + + + + Initializes a new instance of the class. + + + + + Specifies that the output will be non- if the + named parameter is non-. + + + + + Gets the associated parameter name. + The output will be non- if the argument to the + parameter specified is non-. + + + + + Initializes the attribute with the associated parameter name. + + + The associated parameter name. + The output will be non- if the argument to the + parameter specified is non-. + + + + + Specifies that when a method returns , + the parameter will not be even if the corresponding type allows it. + + + + + Gets the return value condition. + If the method returns this value, the associated parameter will not be . + + + + + Initializes the attribute with the specified return value condition. + + + The return value condition. + If the method returns this value, the associated parameter will not be . + + diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 1571feb..1800689 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -33,6 +33,10 @@ namespace CryptoExchange.Net.Interfaces /// event Action, IEnumerable> OnOrderBookUpdate; /// + /// Event when the BestBid or BestAsk changes ie a Pricing Tick + /// + event Action OnPriceChanged; + /// /// Timestamp of the last update /// DateTime LastOrderBookUpdate { get; } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index cc890c8..b33512d 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -51,7 +51,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The status of the order book. Order book is up to date when the status is `Synced` /// - public OrderBookStatus Status + public OrderBookStatus Status { get => status; set @@ -79,6 +79,12 @@ namespace CryptoExchange.Net.OrderBook /// Event when the state changes /// public event Action? OnStatusChange; + + /// + /// Event when the BestBid or BestAsk changes ie a Pricing Tick + /// + public event Action? OnPriceChanged; + /// /// Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets /// @@ -112,7 +118,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The list of bids /// - public IEnumerable Bids + public IEnumerable Bids { get { @@ -136,7 +142,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The best ask currently in the order book /// - public ISymbolOrderBookEntry BestAsk + public ISymbolOrderBookEntry BestAsk { get { @@ -286,9 +292,16 @@ namespace CryptoExchange.Net.OrderBook log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{orderBookSequenceNumber}"); CheckProcessBuffer(); OnOrderBookUpdate?.Invoke(bidList, askList); + OnPriceChanged?.Invoke(BestBid, BestAsk); } } + private bool BestPricingUpdated(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) + { + return BestBid.Price != prevBestBid.Price || BestBid.Quantity != prevBestBid.Quantity || + BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity; + } + /// /// Update the order book using a single id for an update /// @@ -315,8 +328,12 @@ namespace CryptoExchange.Net.OrderBook else { CheckProcessBuffer(); + var prevBestBid = BestBid; + var prevBestAsk = BestAsk; ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); + if (BestPricingUpdated(prevBestBid, prevBestAsk)) + OnPriceChanged?.Invoke(BestBid, BestAsk); } } } @@ -349,8 +366,12 @@ namespace CryptoExchange.Net.OrderBook else { CheckProcessBuffer(); + var prevBestBid = BestBid; + var prevBestAsk = BestAsk; ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); + if (BestPricingUpdated(prevBestBid, prevBestAsk)) + OnPriceChanged?.Invoke(BestBid, BestAsk); } } } @@ -376,8 +397,13 @@ namespace CryptoExchange.Net.OrderBook else { CheckProcessBuffer(); + var prevBestBid = BestBid; + var prevBestAsk = BestAsk; ProcessUpdates(bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); + if (BestPricingUpdated(prevBestBid, prevBestAsk)) + OnPriceChanged?.Invoke(BestBid, BestAsk); + } } } From c870370ebbe180e3a2955fe7001d05a8c07994fc Mon Sep 17 00:00:00 2001 From: Mykyta Kochetkov Date: Tue, 17 Dec 2019 14:22:20 +0100 Subject: [PATCH 32/41] fix log with request body --- CryptoExchange.Net/RestClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 521f9b9..048cf6c 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -181,7 +181,7 @@ namespace CryptoExchange.Net } string? paramString = null; - if (parameters != null && method == HttpMethod.Post) + if (method == HttpMethod.Post) paramString = " with request body " + request.Content; log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null? "": $" via proxy {apiProxy.Host}")}"); From a5a7c4ed1d28a43fb8a4360e6827ca5f125695ad Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 23 Jan 2020 13:52:04 +0100 Subject: [PATCH 33/41] used async await for tasks instead of WaitAll --- CryptoExchange.Net/SocketClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index b416553..c1848ef 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -552,7 +552,7 @@ namespace CryptoExchange.Net { log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.HandlerCount)} subscriptions"); - await Task.Run(() => + await Task.Run(async () => { var tasks = new List(); { @@ -561,7 +561,7 @@ namespace CryptoExchange.Net tasks.Add(sub.Close()); } - Task.WaitAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); }).ConfigureAwait(false); } From ccaf1da5c9ed319eb1ca853191b61b4fc0505315 Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 23 Jan 2020 14:00:58 +0100 Subject: [PATCH 34/41] Renamed OnPriceChanged to OnBestOffersChanged --- CryptoExchange.Net/CryptoExchange.Net.xml | 4 ++-- .../Interfaces/ISymbolOrderBook.cs | 2 +- .../OrderBook/SymbolOrderBook.cs | 21 ++++++++----------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index eb9478e..a29cf01 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -797,7 +797,7 @@ Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets - + Event when the BestBid or BestAsk changes ie a Pricing Tick @@ -1774,7 +1774,7 @@ Event when the state changes - + Event when the BestBid or BestAsk changes ie a Pricing Tick diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 1800689..9d0b502 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -35,7 +35,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Event when the BestBid or BestAsk changes ie a Pricing Tick /// - event Action OnPriceChanged; + event Action OnBestOffersChanged; /// /// Timestamp of the last update /// diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index b33512d..36e94b8 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -83,7 +83,7 @@ namespace CryptoExchange.Net.OrderBook /// /// Event when the BestBid or BestAsk changes ie a Pricing Tick /// - public event Action? OnPriceChanged; + public event Action? OnBestOffersChanged; /// /// Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets @@ -292,14 +292,15 @@ namespace CryptoExchange.Net.OrderBook log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{orderBookSequenceNumber}"); CheckProcessBuffer(); OnOrderBookUpdate?.Invoke(bidList, askList); - OnPriceChanged?.Invoke(BestBid, BestAsk); + OnBestOffersChanged?.Invoke(BestBid, BestAsk); } } - private bool BestPricingUpdated(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) + private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) { - return BestBid.Price != prevBestBid.Price || BestBid.Quantity != prevBestBid.Quantity || - BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity; + if (BestBid.Price != prevBestBid.Price || BestBid.Quantity != prevBestBid.Quantity || + BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity) + OnBestOffersChanged?.Invoke(BestBid, BestAsk); } /// @@ -332,8 +333,7 @@ namespace CryptoExchange.Net.OrderBook var prevBestAsk = BestAsk; ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); - if (BestPricingUpdated(prevBestBid, prevBestAsk)) - OnPriceChanged?.Invoke(BestBid, BestAsk); + CheckBestOffersChanged(prevBestBid, prevBestAsk); } } } @@ -370,8 +370,7 @@ namespace CryptoExchange.Net.OrderBook var prevBestAsk = BestAsk; ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); - if (BestPricingUpdated(prevBestBid, prevBestAsk)) - OnPriceChanged?.Invoke(BestBid, BestAsk); + CheckBestOffersChanged(prevBestBid, prevBestAsk); } } } @@ -401,9 +400,7 @@ namespace CryptoExchange.Net.OrderBook var prevBestAsk = BestAsk; ProcessUpdates(bids, asks); OnOrderBookUpdate?.Invoke(bids, asks); - if (BestPricingUpdated(prevBestBid, prevBestAsk)) - OnPriceChanged?.Invoke(BestBid, BestAsk); - + CheckBestOffersChanged(prevBestBid, prevBestAsk); } } } From 400259c0a1dd08e5ed2f51a8c15d0f7bf988fa21 Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 23 Jan 2020 14:14:28 +0100 Subject: [PATCH 35/41] Moved error logging so no error are logged when error response is being parsed --- CryptoExchange.Net/BaseClient.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 8c32f2b..a4bbc98 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -106,19 +106,16 @@ namespace CryptoExchange.Net catch (JsonReaderException jre) { var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {data}"; - log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } catch (JsonSerializationException jse) { var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {data}"; - log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } catch (Exception ex) { var info = $"Deserialize Unknown Exception: {ex.Message}. Data: {data}"; - log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } } @@ -134,7 +131,13 @@ namespace CryptoExchange.Net protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer? serializer = null) { var tokenResult = ValidateJson(data); - return !tokenResult ? new CallResult(default, tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer); + if (!tokenResult) + { + log.Write(LogVerbosity.Error, tokenResult.Error!.Message); + return new CallResult(default, tokenResult.Error); + } + + return Deserialize(tokenResult.Data, checkObject, serializer); } /// From 649860b4d8cb7a85d0956c97deb08dc436d13566 Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 23 Jan 2020 15:18:05 +0100 Subject: [PATCH 36/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 4 ++-- README.md | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index b772e21..188bc11 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -5,12 +5,12 @@ CryptoExchange.Net JKorf - 3.0.2 + 3.0.3 false https://github.com/JKorf/CryptoExchange.Net en true - 3.0.2 - Removed invalid check for unauthenticated proxy + 3.0.3 - Added OnBestOffersChanged event to order book implementations enable 8.0 MIT diff --git a/README.md b/README.md index 970fd4b..4dfa2ed 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,9 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 3.0.3 - 23 Jan 2020 + * Added OnBestOffersChanged event to order book implementations + * Version 3.0.2 - 10 Dec 2019 * Removed invalid check for unauthenticated proxy From 82ff1aebafea43874db51f4e1c5796da6f828cc3 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 29 Jan 2020 17:05:28 +0100 Subject: [PATCH 37/41] SocketConnection.Send check if object is string, if so it doesn't deserialize again --- CryptoExchange.Net/CryptoExchange.Net.csproj | 1 + CryptoExchange.Net/CryptoExchange.Net.xml | 143 ------------------ .../Sockets/SocketConnection.cs | 5 +- 3 files changed, 5 insertions(+), 144 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 188bc11..ac224ad 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -5,6 +5,7 @@ CryptoExchange.Net JKorf + A base package for implementing cryptocurrency exchange API's 3.0.3 false https://github.com/JKorf/CryptoExchange.Net diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index a29cf01..62e5487 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -2898,148 +2898,5 @@ - - - Specifies that is allowed as an input even if the - corresponding type disallows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that is disallowed as an input even if the - corresponding type allows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that a method that will never return under any circumstance. - - - - - Initializes a new instance of the class. - - - - - Specifies that the method will not return if the associated - parameter is passed the specified value. - - - - - Gets the condition parameter value. - Code after the method is considered unreachable by diagnostics if the argument - to the associated parameter matches this value. - - - - - Initializes a new instance of the - class with the specified parameter value. - - - The condition parameter value. - Code after the method is considered unreachable by diagnostics if the argument - to the associated parameter matches this value. - - - - - Specifies that an output may be even if the - corresponding type disallows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that when a method returns , - the parameter may be even if the corresponding type disallows it. - - - - - Gets the return value condition. - If the method returns this value, the associated parameter may be . - - - - - Initializes the attribute with the specified return value condition. - - - The return value condition. - If the method returns this value, the associated parameter may be . - - - - - Specifies that an output is not even if the - corresponding type allows it. - - - - - Initializes a new instance of the class. - - - - - Specifies that the output will be non- if the - named parameter is non-. - - - - - Gets the associated parameter name. - The output will be non- if the argument to the - parameter specified is non-. - - - - - Initializes the attribute with the associated parameter name. - - - The associated parameter name. - The output will be non- if the argument to the - parameter specified is non-. - - - - - Specifies that when a method returns , - the parameter will not be even if the corresponding type allows it. - - - - - Gets the return value condition. - If the method returns this value, the associated parameter will not be . - - - - - Initializes the attribute with the specified return value condition. - - - The return value condition. - If the method returns this value, the associated parameter will not be . - - diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index b7e8b9a..e1704e0 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -216,7 +216,10 @@ namespace CryptoExchange.Net.Sockets /// How null values should be serialized public virtual void Send(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore) { - Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling })); + if(obj is string str) + Send(str); + else + Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling })); } /// From d4591023236cafd1c6ef2d77bbed79d5c3e6458e Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 29 Jan 2020 17:06:46 +0100 Subject: [PATCH 38/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index ac224ad..841462b 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -6,12 +6,12 @@ CryptoExchange.Net JKorf A base package for implementing cryptocurrency exchange API's - 3.0.3 + 3.0.4 false https://github.com/JKorf/CryptoExchange.Net en true - 3.0.3 - Added OnBestOffersChanged event to order book implementations + 3.0.4 - Removed unnecessary json serialization when sending string data enable 8.0 MIT From 66c0b9001611cbf165b603020f5c906407f641c6 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 5 Feb 2020 11:26:36 +0100 Subject: [PATCH 39/41] Added paused activity socket events --- CryptoExchange.Net/CryptoExchange.Net.xml | 20 ++++++++++++++ CryptoExchange.Net/SocketClient.cs | 5 ++++ .../Sockets/SocketConnection.cs | 27 +++++++++++++++++-- .../Sockets/UpdateSubscription.cs | 18 +++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 62e5487..97639e7 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -2689,6 +2689,16 @@ Connecting restored event + + + The connection is paused event + + + + + The connection is unpaused event + + Connecting closed event @@ -2858,6 +2868,16 @@ Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting + + + Event when the connection to the server is paused. No operations can be performed while paused + + + + + Event when the connection to the server is unpaused + + Event when an exception happened diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index c1848ef..77ea973 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -161,6 +161,11 @@ namespace CryptoExchange.Net semaphoreSlim.Release(); } + if (socket.PausedActivity) + { + log.Write(LogVerbosity.Info, "Socket has been paused, can't subscribe at this moment"); + return new CallResult(default, new ServerError("Socket is paused")); + } if (request != null) { diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index e1704e0..8b6e2c8 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -25,6 +25,14 @@ namespace CryptoExchange.Net.Sockets /// public event Action? ConnectionRestored; /// + /// The connection is paused event + /// + public event Action? ActivityPaused; + /// + /// The connection is unpaused event + /// + public event Action? ActivityUnpaused; + /// /// Connecting closed event /// public event Action? Closed; @@ -60,11 +68,26 @@ namespace CryptoExchange.Net.Sockets /// Time of disconnecting /// public DateTime? DisconnectTime { get; set; } + /// /// If activity is paused /// - public bool PausedActivity { get; set; } + public bool PausedActivity + { + get => pausedActivity; + set + { + if (pausedActivity != value) + { + pausedActivity = value; + log.Write(LogVerbosity.Debug, "Paused activity: " + value); + if(pausedActivity) ActivityPaused?.Invoke(); + else ActivityUnpaused?.Invoke(); + } + } + } + private bool pausedActivity; private readonly List handlers; private readonly object handlersLock = new object(); @@ -155,7 +178,7 @@ namespace CryptoExchange.Net.Sockets var sw = Stopwatch.StartNew(); lock (handlersLock) { - foreach (var handler in handlers) + foreach (var handler in handlers.ToList()) { currentSubscription = handler; if (handler.Request == null) diff --git a/CryptoExchange.Net/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Sockets/UpdateSubscription.cs index 681b0c5..98f37b4 100644 --- a/CryptoExchange.Net/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Sockets/UpdateSubscription.cs @@ -29,6 +29,24 @@ namespace CryptoExchange.Net.Sockets remove => connection.ConnectionRestored -= value; } + /// + /// Event when the connection to the server is paused. No operations can be performed while paused + /// + public event Action ActivityPaused + { + add => connection.ActivityPaused += value; + remove => connection.ActivityPaused -= value; + } + + /// + /// Event when the connection to the server is unpaused + /// + public event Action ActivityUnpaused + { + add => connection.ActivityUnpaused += value; + remove => connection.ActivityUnpaused -= value; + } + /// /// Event when an exception happened /// From 84fe4c374a06e8712c1a593bc512222a6aeae6b2 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 5 Feb 2020 11:38:10 +0100 Subject: [PATCH 40/41] Updated version --- CryptoExchange.Net/CryptoExchange.Net.csproj | 6 +++--- README.md | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 841462b..6c4f1cb 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -5,13 +5,13 @@ CryptoExchange.Net JKorf - A base package for implementing cryptocurrency exchange API's - 3.0.4 + A base package for implementing cryptocurrency exchange API's + 3.0.5 false https://github.com/JKorf/CryptoExchange.Net en true - 3.0.4 - Removed unnecessary json serialization when sending string data + 3.0.5 - Added PausedActivity events on socket subscriptions enable 8.0 MIT diff --git a/README.md b/README.md index 4dfa2ed..5691756 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,9 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 3.0.5 - 05 Feb 2020 + * Added PausedActivity events on socket subscriptions + * Version 3.0.3 - 23 Jan 2020 * Added OnBestOffersChanged event to order book implementations From 81c5abd426e4fc73f1fcae3b7a8d9d1f26b9acc3 Mon Sep 17 00:00:00 2001 From: JKorf Date: Wed, 5 Feb 2020 11:40:32 +0100 Subject: [PATCH 41/41] Added missing history --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5691756..f7b667c 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,9 @@ To stop synchronizing an order book use the `Stop` method. * Version 3.0.5 - 05 Feb 2020 * Added PausedActivity events on socket subscriptions +* Version 3.0.4 - 29 Jan 2020 + * Removed unnecessary json serialization + * Version 3.0.3 - 23 Jan 2020 * Added OnBestOffersChanged event to order book implementations
+
Bittrex
+
Bitfinex
+
Binance
+
CoinEx
+
Huobi
+
Kucoin
+
Kraken