diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 7e143d4..55e9d80 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -49,11 +49,11 @@ namespace CryptoExchange.Net.Authentication /// The method of the request /// If the requests should be authenticated /// Array serialization type - /// The position where the providedParameters should go /// The formatting of the request body /// Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri /// Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body /// The headers that should be send with the request + /// The position where the providedParameters should go public abstract void AuthenticateRequest( RestApiClient apiClient, Uri uri, diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 68c2af0..857879d 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -150,23 +150,60 @@ namespace CryptoExchange.Net.Clients /// Additional headers for this request /// Override the request weight for this request definition, for example when the weight depends on the parameters /// - protected virtual async Task> SendAsync( + protected virtual Task> SendAsync( string baseAddress, RequestDefinition definition, ParameterCollection? parameters, CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null) where T : class + { + var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; + return SendAsync( + baseAddress, + definition, + parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null, + parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, + cancellationToken, + additionalHeaders, + weight); + } + + /// + /// Send a request to the base address based on the request definition + /// + /// Response type + /// Host and schema + /// Request definition + /// Request query parameters + /// Request body parameters + /// Cancellation token + /// Additional headers for this request + /// Override the request weight for this request definition, for example when the weight depends on the parameters + /// + protected virtual async Task> SendAsync( + string baseAddress, + RequestDefinition definition, + ParameterCollection? uriParameters, + ParameterCollection? bodyParameters, + CancellationToken cancellationToken, + Dictionary? additionalHeaders = null, + int? weight = null) where T : class { int currentTry = 0; while (true) { currentTry++; - var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); + var prepareResult = await PrepareAsync(baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); if (!prepareResult) return new WebCallResult(prepareResult.Error!); - var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders); + var request = CreateRequest( + baseAddress, + definition, + uriParameters, + bodyParameters, + additionalHeaders); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); TotalRequestsMade++; var result = await GetResponseAsync(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); @@ -187,7 +224,6 @@ namespace CryptoExchange.Net.Clients /// /// Host and schema /// Request definition - /// Request parameters /// Cancellation token /// Additional headers for this request /// Override the request weight for this request @@ -196,7 +232,6 @@ namespace CryptoExchange.Net.Clients protected virtual async Task PrepareAsync( string baseAddress, RequestDefinition definition, - ParameterCollection? parameters, CancellationToken cancellationToken, Dictionary? additionalHeaders = null, int? weight = null) @@ -264,25 +299,27 @@ namespace CryptoExchange.Net.Clients /// /// Host and schema /// Request definition - /// The parameters of the request + /// The query parameters of the request + /// The body parameters of the request /// Additional headers to send with the request /// protected virtual IRequest CreateRequest( string baseAddress, RequestDefinition definition, - ParameterCollection? parameters, + ParameterCollection? uriParameters, + ParameterCollection? bodyParameters, Dictionary? additionalHeaders) { - parameters ??= new ParameterCollection(); + var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters); + var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters); + var uri = new Uri(baseAddress.AppendPath(definition.Path)); - var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var arraySerialization = definition.ArraySerialization ?? ArraySerialization; var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat; var requestId = ExchangeHelpers.NextId(); + var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var headers = new Dictionary(); - var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary(); - var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary(); if (AuthenticationProvider != null) { try @@ -291,13 +328,14 @@ namespace CryptoExchange.Net.Clients this, uri, definition.Method, - uriParameters, - bodyParameters, + uriParams, + bodyParams, headers, definition.Authenticated, arraySerialization, parameterPosition, - bodyFormat); + bodyFormat + ); } catch (Exception ex) { @@ -305,18 +343,8 @@ namespace CryptoExchange.Net.Clients } } - // Sanity check - foreach (var param in parameters) - { - if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key)) - { - throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " + - $"should return provided parameters in either the uri or body parameters output"); - } - } - // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters - uri = uri.SetParameters(uriParameters, arraySerialization); + uri = uri.SetParameters(uriParams, arraySerialization); var request = RequestFactory.Create(definition.Method, uri, requestId); request.Accept = Constants.JsonContentHeader; @@ -343,8 +371,8 @@ namespace CryptoExchange.Net.Clients if (parameterPosition == HttpMethodParameterPosition.InBody) { var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; - if (bodyParameters.Count != 0) - WriteParamBody(request, bodyParameters, contentType); + if (bodyParams.Count != 0) + WriteParamBody(request, bodyParams, contentType); else request.SetContent(RequestBodyEmptyContent, contentType); } @@ -739,7 +767,8 @@ namespace CryptoExchange.Net.Clients signed, arraySerialization, parameterPosition, - bodyFormat); + bodyFormat + ); } catch (Exception ex) { diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 7f29850..406387d 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -278,10 +278,11 @@ namespace CryptoExchange.Net.Clients /// /// Send a query on a socket connection to the BaseAddress and wait for the response /// - /// Expected result type + /// Expected result type + /// The type returned to the caller /// The query /// - protected virtual Task> QueryAsync(Query query) + protected virtual Task> QueryAsync(Query query) { return QueryAsync(BaseAddress, query); } @@ -289,14 +290,15 @@ namespace CryptoExchange.Net.Clients /// /// Send a query on a socket connection and wait for the response /// - /// The expected result type + /// Expected result type + /// The type returned to the caller /// The url for the request /// The query /// - protected virtual async Task> QueryAsync(string url, Query query) + protected virtual async Task> QueryAsync(string url, Query query) { if (_disposing) - return new CallResult(new InvalidOperationError("Client disposed, can't query")); + return new CallResult(new InvalidOperationError("Client disposed, can't query")); SocketConnection socketConnection; var released = false; @@ -305,7 +307,7 @@ namespace CryptoExchange.Net.Clients { var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false); if (!socketResult) - return socketResult.As(default); + return socketResult.As(default); socketConnection = socketResult.Data; @@ -318,7 +320,7 @@ namespace CryptoExchange.Net.Clients var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false); if (!connectResult) - return new CallResult(connectResult.Error!); + return new CallResult(connectResult.Error!); } finally { @@ -329,10 +331,10 @@ namespace CryptoExchange.Net.Clients if (socketConnection.PausedActivity) { _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError("Socket is paused")); + return new CallResult(new ServerError("Socket is paused")); } - return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false); + return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false); } /// diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs index da52d66..5d480a1 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -154,6 +154,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue)) { // Parse 1637745563.000 format + if (doubleValue <= 0) + return default; if (doubleValue < 19999999999) return ConvertFromSeconds(doubleValue); if (doubleValue < 19999999999999) diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index 9ab3b10..f0ee044 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -68,6 +68,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); } + catch (Exception ex) + { + var info = $"Unknown exception: {ex.Message}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } } /// diff --git a/CryptoExchange.Net/Objects/Sockets/DataEvent.cs b/CryptoExchange.Net/Objects/Sockets/DataEvent.cs index bfd9db3..d627fcc 100644 --- a/CryptoExchange.Net/Objects/Sockets/DataEvent.cs +++ b/CryptoExchange.Net/Objects/Sockets/DataEvent.cs @@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets public DateTime Timestamp { get; set; } /// - /// The topic of the update, what symbol/asset etc.. + /// The stream producing the update /// - public string? Topic { get; set; } + public string? StreamId { get; set; } + + /// + /// The symbol the update is for + /// + public string? Symbol { get; set; } /// /// The original data that was received, only available when OutputOriginalData is set to true in the client options @@ -33,10 +38,11 @@ namespace CryptoExchange.Net.Objects.Sockets /// public T Data { get; set; } - internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType) + internal DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType) { Data = data; - Topic = topic; + StreamId = streamId; + Symbol = symbol; OriginalData = originalData; Timestamp = timestamp; UpdateType = updateType; @@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets /// public DataEvent As(K data) { - return new DataEvent(data, Topic, OriginalData, Timestamp, UpdateType); + return new DataEvent(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType); } /// @@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets /// /// The type of the new data /// The new data - /// The new topic + /// The new symbol /// - public DataEvent As(K data, string? topic) + public DataEvent As(K data, string? symbol) { - return new DataEvent(data, topic, OriginalData, Timestamp, UpdateType); + return new DataEvent(data, StreamId, symbol, OriginalData, Timestamp, UpdateType); } /// @@ -70,12 +76,73 @@ namespace CryptoExchange.Net.Objects.Sockets /// /// The type of the new data /// The new data - /// The new topic + /// The new stream id + /// The new symbol /// The type of update /// - public DataEvent As(K data, string? topic, SocketUpdateType updateType) + public DataEvent As(K data, string streamId, string? symbol, SocketUpdateType updateType) { - return new DataEvent(data, topic, OriginalData, Timestamp, updateType); + return new DataEvent(data, streamId, symbol, OriginalData, Timestamp, updateType); + } + + /// + /// Specify the symbol + /// + /// + /// + public DataEvent WithSymbol(string symbol) + { + Symbol = symbol; + return this; + } + + /// + /// Specify the update type + /// + /// + /// + public DataEvent WithUpdateType(SocketUpdateType type) + { + UpdateType = type; + return this; + } + + /// + /// Specify the stream id + /// + /// + /// + public DataEvent WithStreamId(string streamId) + { + StreamId = streamId; + return this; + } + + /// + /// Create a CallResult from this DataEvent + /// + /// + public CallResult ToCallResult() + { + return new CallResult(Data, OriginalData, null); + } + + /// + /// Create a CallResult from this DataEvent + /// + /// + public CallResult ToCallResult(K data) + { + return new CallResult(data, OriginalData, null); + } + + /// + /// Create a CallResult from this DataEvent + /// + /// + public CallResult ToCallResult(Error error) + { + return new CallResult(default, OriginalData, error); } } } diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index b909870..689bcc5 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Requests if (client == null) { var handler = new HttpClientHandler(); + handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; if (proxy != null) { handler.Proxy = new WebProxy diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 2c9148c..149c17d 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -1,6 +1,7 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Requests; using System; using System.Collections.Generic; using System.Threading; @@ -145,16 +146,17 @@ namespace CryptoExchange.Net.Sockets /// /// Query /// - /// Response object type - public abstract class Query : Query + /// The type returned from the server + /// The type to be returned to the caller + public abstract class Query : Query { /// - public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse); + public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse); /// /// The typed call result /// - public CallResult? TypedResult => (CallResult?)Result; + public CallResult? TypedResult => (CallResult?)Result; /// /// ctor @@ -171,7 +173,7 @@ namespace CryptoExchange.Net.Sockets { Completed = true; Response = message.Data; - Result = HandleMessage(connection, message.As((TResponse)message.Data)); + Result = HandleMessage(connection, message.As((TServerResponse)message.Data)); _event.Set(); ContinueAwaiter?.WaitOne(); return Result; @@ -183,7 +185,7 @@ namespace CryptoExchange.Net.Sockets /// /// /// - public virtual CallResult HandleMessage(SocketConnection connection, DataEvent message) => new CallResult(message.Data, message.OriginalData, null); + public abstract CallResult HandleMessage(SocketConnection connection, DataEvent message); /// public override void Timeout() @@ -192,7 +194,7 @@ namespace CryptoExchange.Net.Sockets return; Completed = true; - Result = new CallResult(new CancellationRequestedError(null, "Query timeout", null)); + Result = new CallResult(new CancellationRequestedError(null, "Query timeout", null)); ContinueAwaiter?.Set(); _event.Set(); } @@ -200,10 +202,35 @@ namespace CryptoExchange.Net.Sockets /// public override void Fail(Error error) { - Result = new CallResult(error); + Result = new CallResult(error); Completed = true; ContinueAwaiter?.Set(); _event.Set(); } } + + /// + /// Query + /// + /// Response object type + public abstract class Query : Query + { + /// + /// ctor + /// + /// + /// + /// + protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight) + { + } + + /// + /// Handle the query response + /// + /// + /// + /// + public override CallResult HandleMessage(SocketConnection connection, DataEvent message) => message.ToCallResult(); + } } diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 5d2aaff..13deab3 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -498,7 +498,7 @@ namespace CryptoExchange.Net.Sockets try { var innerSw = Stopwatch.StartNew(); - processor.Handle(this, new DataEvent(deserialized, null, originalData, receiveTime, null)); + processor.Handle(this, new DataEvent(deserialized, null, null, originalData, receiveTime, null)); totalUserTime += (int)innerSw.ElapsedMilliseconds; } catch (Exception ex) @@ -696,14 +696,15 @@ namespace CryptoExchange.Net.Sockets /// /// Send a query request and wait for an answer /// - /// Query response type + /// Expected result type + /// The type returned to the caller /// Query to send /// Wait event for when the socket message handler can continue /// - public virtual async Task> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null) + public virtual async Task> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null) { await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false); - return query.TypedResult ?? new CallResult(new ServerError("Timeout")); + return query.TypedResult ?? new CallResult(new ServerError("Timeout")); } private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent) diff --git a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs index af39185..ae87c81 100644 --- a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs +++ b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs @@ -52,8 +52,13 @@ namespace CryptoExchange.Net.Testing.Comparers else { if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) + { + if (dictProp.Value.ToString() == "") + continue; + // Property value not correct throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"); + } } } } @@ -162,7 +167,7 @@ namespace CryptoExchange.Net.Testing.Comparers if (dictProp.Value.Type == JTokenType.Object) { - CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties); + CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties); } else { @@ -180,7 +185,10 @@ namespace CryptoExchange.Net.Testing.Comparers var enumerator = list.GetEnumerator(); foreach (JToken jtoken in jObjs) { - enumerator.MoveNext(); + var moved = enumerator.MoveNext(); + if (!moved) + throw new Exception("Enumeration not moved; incorrect amount of results?"); + var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter)) // Custom converter for the type, skip @@ -260,9 +268,9 @@ namespace CryptoExchange.Net.Testing.Comparers else if (objectValue is DateTime time) { if (time != DateTimeConverter.ParseFromString(jsonValue.Value()!)) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {time}"); + throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {time}"); } - else if (propertyType.IsEnum) + else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) { // TODO enum comparing } @@ -278,6 +286,10 @@ namespace CryptoExchange.Net.Testing.Comparers if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value()!)) throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {time}"); } + else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) + { + // TODO enum comparing + } else if (jsonValue.Value() != Convert.ToInt64(objectValue)) { throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {Convert.ToInt64(objectValue)}"); diff --git a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs index 7b1d7be..8a7ae06 100644 --- a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs +++ b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs @@ -109,6 +109,7 @@ namespace CryptoExchange.Net.Testing if (lastMessage == null) throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); + var lastMessageJson = JToken.Parse(lastMessage); var expectedJson = JToken.Parse(line.Substring(2)); foreach(var item in expectedJson) @@ -121,6 +122,12 @@ namespace CryptoExchange.Net.Testing overrideKey = val.ToString(); overrideValue = lastMessageJson[prop.Name]?.Value(); } + else if (val.ToString() == "-999") + { + // -999 value is used to replace parts or response messages + overrideKey = val.ToString(); + overrideValue = lastMessageJson[prop.Name]?.Value().ToString(); + } else if (lastMessageJson[prop.Name]?.Value() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true) throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value()}"); } diff --git a/CryptoExchange.Net/Testing/TestHelpers.cs b/CryptoExchange.Net/Testing/TestHelpers.cs index 5360218..c32a75a 100644 --- a/CryptoExchange.Net/Testing/TestHelpers.cs +++ b/CryptoExchange.Net/Testing/TestHelpers.cs @@ -136,8 +136,9 @@ namespace CryptoExchange.Net.Testing headers, true, client.ArraySerialization, - client.ParameterPositions[method], - client.RequestBodyFormat); + client.ParameterPositions[method], + client.RequestBodyFormat + ); var signature = getSignature(uriParams, bodyParams, headers);