diff --git a/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs b/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs index 5acccaf..98b5efb 100644 --- a/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs +++ b/CryptoExchange.Net.Protobuf/Converters/Protobuf/ProtobufMessageAccessor.cs @@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Converters.Protobuf } catch (Exception ex) { - return new CallResult(new DeserializeError(ex.Message)); + return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)); } } @@ -320,7 +320,7 @@ namespace CryptoExchange.Net.Converters.Protobuf } catch(Exception ex) { - return new CallResult(new DeserializeError(ex.ToLogString())); + return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)); } } @@ -354,7 +354,7 @@ namespace CryptoExchange.Net.Converters.Protobuf { // Not a json message IsValid = false; - return Task.FromResult(new CallResult(new DeserializeError("ProtoBufError: " + ex.Message, ex))); + return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex))); } } @@ -446,7 +446,7 @@ namespace CryptoExchange.Net.Converters.Protobuf } catch (Exception ex) { - return new CallResult(new DeserializeError(ex.ToLogString())); + return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)); } } @@ -485,7 +485,7 @@ namespace CryptoExchange.Net.Converters.Protobuf } catch (Exception ex) { - return new CallResult(new DeserializeError(ex.Message)); + return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)); } } @@ -504,7 +504,7 @@ namespace CryptoExchange.Net.Converters.Protobuf { // Not a json message IsValid = false; - return new CallResult(new DeserializeError("ProtobufError: " + ex.Message, ex)); + return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)); } } diff --git a/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj index 148127f..287bbae 100644 --- a/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj +++ b/CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.csproj @@ -41,7 +41,9 @@ CryptoExchange.Net.Protobuf.xml - + + + \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/CallResultTests.cs b/CryptoExchange.Net.UnitTests/CallResultTests.cs index ab75100..8cf0ab6 100644 --- a/CryptoExchange.Net.UnitTests/CallResultTests.cs +++ b/CryptoExchange.Net.UnitTests/CallResultTests.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using NUnit.Framework; using NUnit.Framework.Legacy; using System; @@ -16,9 +17,9 @@ namespace CryptoExchange.Net.UnitTests [Test] public void TestBasicErrorCallResult() { - var result = new CallResult(new ServerError("TestError")); + var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); - ClassicAssert.AreSame(result.Error.Message, "TestError"); + ClassicAssert.AreSame(result.Error.ErrorCode, "TestError"); ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result.Success); } @@ -36,9 +37,9 @@ namespace CryptoExchange.Net.UnitTests [Test] public void TestCallResultError() { - var result = new CallResult(new ServerError("TestError")); + var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); - ClassicAssert.AreSame(result.Error.Message, "TestError"); + ClassicAssert.AreSame(result.Error.ErrorCode, "TestError"); ClassicAssert.IsNull(result.Data); ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result.Success); @@ -71,11 +72,11 @@ namespace CryptoExchange.Net.UnitTests [Test] public void TestCallResultErrorAs() { - var result = new CallResult(new ServerError("TestError")); + var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); var asResult = result.As(default); ClassicAssert.IsNotNull(asResult.Error); - ClassicAssert.AreSame(asResult.Error.Message, "TestError"); + ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError"); ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult.Success); @@ -84,11 +85,11 @@ namespace CryptoExchange.Net.UnitTests [Test] public void TestCallResultErrorAsError() { - var result = new CallResult(new ServerError("TestError")); - var asResult = result.AsError(new ServerError("TestError2")); + var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); + var asResult = result.AsError(new ServerError("TestError2", ErrorInfo.Unknown)); ClassicAssert.IsNotNull(asResult.Error); - ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); + ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2"); ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult.Success); @@ -97,11 +98,11 @@ namespace CryptoExchange.Net.UnitTests [Test] public void TestWebCallResultErrorAsError() { - var result = new WebCallResult(new ServerError("TestError")); - var asResult = result.AsError(new ServerError("TestError2")); + var result = new WebCallResult(new ServerError("TestError", ErrorInfo.Unknown)); + var asResult = result.AsError(new ServerError("TestError2", ErrorInfo.Unknown)); ClassicAssert.IsNotNull(asResult.Error); - ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); + ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2"); ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult.Success); @@ -124,10 +125,10 @@ namespace CryptoExchange.Net.UnitTests ResultDataSource.Server, new TestObjectResult(), null); - var asResult = result.AsError(new ServerError("TestError2")); + var asResult = result.AsError(new ServerError("TestError2", ErrorInfo.Unknown)); ClassicAssert.IsNotNull(asResult.Error); - Assert.That(asResult.Error.Message == "TestError2"); + Assert.That(asResult.Error.ErrorCode == "TestError2"); Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK); Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); Assert.That(asResult.RequestUrl == "https://test.com/api"); diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index ded742f..1a93a3a 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests ClassicAssert.IsFalse(result.Success); Assert.That(result.Error != null); Assert.That(result.Error is ServerError); - Assert.That(result.Error.Code == 123); + Assert.That(result.Error.ErrorCode == "123"); Assert.That(result.Error.Message == "Invalid request"); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs index 212c055..431dd7a 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; using System; @@ -40,7 +41,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets { if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase)) { - return new CallResult(new ServerError(message.Data.Status)); + return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status })); } return message.ToCallResult(); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 52dd6a4..09df970 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -10,6 +10,7 @@ using CryptoExchange.Net.Clients; using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.UnitTests.TestImplementations; @@ -55,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests var accessor = CreateAccessor(); var valid = accessor.Read(stream, true).Result; if (!valid) - return new CallResult(new ServerError(data)); + return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = data })); var deserializeResult = accessor.Deserialize(); return deserializeResult; diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index f909ed9..ed1227c 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Options; using System.Linq; using CryptoExchange.Net.Converters.SystemTextJson; using System.Text.Json.Serialization; +using CryptoExchange.Net.Objects.Errors; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -197,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { var errorData = accessor.Deserialize(); - return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage); + return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage)); } public override TimeSpan? GetTimeOffset() diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index a4f64c0..7806e1f 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.Logging; @@ -54,6 +56,11 @@ namespace CryptoExchange.Net.Clients /// public ExchangeOptions ClientOptions { get; } + /// + /// Mapping of a response code to known error types + /// + protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]); + /// /// ctor /// @@ -87,6 +94,16 @@ namespace CryptoExchange.Net.Clients /// public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); + /// + /// Get error info for a response code + /// + public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message); + + /// + /// Get error info for a response code + /// + public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message); + /// public void SetApiCredentials(T credentials) where T : ApiCredentials { diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 3d3532e..c368e3d 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -11,6 +11,7 @@ using CryptoExchange.Net.Caching; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting.Interfaces; @@ -485,8 +486,10 @@ namespace CryptoExchange.Net.Clients error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); } +#pragma warning disable CS0618 // Type or member is obsolete if (error.Code == null || error.Code == 0) error.Code = (int)response.StatusCode; +#pragma warning restore CS0618 // Type or member is obsolete return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); } @@ -499,8 +502,7 @@ namespace CryptoExchange.Net.Clients if (!valid) { // Invalid json - var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error); } // Json response received @@ -526,7 +528,8 @@ namespace CryptoExchange.Net.Clients catch (HttpRequestException requestException) { // Request exception, can't reach server for instance - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException)); + var error = new WebError(requestException.Message, requestException); + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } catch (OperationCanceledException canceledException) { @@ -538,7 +541,9 @@ namespace CryptoExchange.Net.Clients else { // Request timed out - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException)); + var error = new WebError($"Request timed out", exception: canceledException); + error.ErrorType = ErrorType.Timeout; + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); } } finally @@ -633,7 +638,7 @@ namespace CryptoExchange.Net.Clients /// protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) { - return new ServerError(null, "Unknown request error", exception); + return new ServerError(ErrorInfo.Unknown, exception); } /// diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index fcc0565..faac4b1 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -1,6 +1,7 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.RateLimiting; @@ -265,7 +266,7 @@ namespace CryptoExchange.Net.Clients if (socketConnection.PausedActivity) { _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError("Socket is paused")); + return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); } var waitEvent = new AsyncResetEvent(false); @@ -368,7 +369,7 @@ namespace CryptoExchange.Net.Clients if (socketConnection.PausedActivity) { _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); - return new CallResult(new ServerError("Socket is paused")); + return new CallResult(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); } if (ct.IsCancellationRequested) diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index 0f9645c..c464f23 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -60,13 +60,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } catch (JsonException ex) { - var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; + var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; return new CallResult(new DeserializeError(info, ex)); } catch (Exception ex) { - var info = $"Deserialize unknown Exception: {ex.Message}"; - return new CallResult(new DeserializeError(info, ex)); + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); } } @@ -87,13 +86,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } catch (JsonException ex) { - var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; + var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; return new CallResult(new DeserializeError(info, ex)); } catch (Exception ex) { - var info = $"Unknown exception: {ex.Message}"; - return new CallResult(new DeserializeError(info, ex)); + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); } } @@ -286,7 +284,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { // Not a json message IsValid = false; - return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); } } @@ -338,7 +336,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { // Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow IsValid = false; - return new CallResult(new ServerError("Not a json value")); + return new CallResult(new DeserializeError("Not a json value")); } _document = JsonDocument.Parse(data); @@ -349,7 +347,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { // Not a json message IsValid = false; - return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); + return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex)); } } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 61a012b..493a875 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -1,4 +1,5 @@ -using System; +using CryptoExchange.Net.Objects.Errors; +using System; namespace CryptoExchange.Net.Objects { @@ -7,15 +8,50 @@ namespace CryptoExchange.Net.Objects /// public abstract class Error { + + private int? _code; /// /// The error code from the server /// - public int? Code { get; set; } + [Obsolete("Use ErrorCode instead", false)] + public int? Code + { + get + { + if (_code.HasValue) + return _code; + + return int.TryParse(ErrorCode, out var r) ? r : null; + } + set + { + _code = value; + } + } /// - /// The message for the error that occurred + /// The error code returned by the server /// - public string Message { get; set; } + public string? ErrorCode { get; set; } + /// + /// The error description + /// + public string? ErrorDescription { get; set; } + + /// + /// Error type + /// + public ErrorType ErrorType { get; set; } + + /// + /// Whether the error is transient and can be retried + /// + public bool IsTransient { get; set; } + + /// + /// The server message for the error that occurred + /// + public string? Message { get; set; } /// /// Underlying exception @@ -25,10 +61,13 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - protected Error (int? code, string message, Exception? exception) + protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception) { - Code = code; - Message = message; + ErrorCode = errorCode; + ErrorType = errorInfo.ErrorType; + Message = errorInfo.Message; + ErrorDescription = errorInfo.ErrorDescription; + IsTransient = errorInfo.IsTransient; Exception = exception; } @@ -38,7 +77,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return Code != null ? $"[{GetType().Name}] {Code}: {Message}" : $"[{GetType().Name}] {Message}"; + return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}"; } } @@ -48,19 +87,24 @@ namespace CryptoExchange.Net.Objects public class CantConnectError : Error { /// - /// ctor + /// Default error info /// - public CantConnectError() : base(null, "Can't connect to the server", null) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.UnableToConnect, false, "Can't connect to the server"); /// /// ctor /// - public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { } + public CantConnectError() : base(null, _errorInfo, null) { } /// /// ctor /// - protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { } + public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { } + + /// + /// ctor + /// + protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } } /// @@ -69,14 +113,19 @@ namespace CryptoExchange.Net.Objects public class NoApiCredentialsError : Error { /// - /// ctor + /// Default error info /// - public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint"); /// /// ctor /// - protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { } + public NoApiCredentialsError() : base(null, _errorInfo, null) { } + + /// + /// ctor + /// + protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } } /// @@ -87,12 +136,19 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - public ServerError(string message) : base(null, message, null) { } + public ServerError(ErrorInfo errorInfo, Exception? exception = null) + : base(null, errorInfo, exception) { } /// /// ctor /// - public ServerError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } + public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null) + : this(errorCode.ToString(), errorInfo, exception) { } + + /// + /// ctor + /// + public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { } } /// @@ -101,14 +157,30 @@ namespace CryptoExchange.Net.Objects public class WebError : Error { /// - /// ctor + /// Default error info /// - public WebError(string message, Exception? exception = null) : base(null, message, exception) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.NetworkError, true, "Failed to complete the request to the server due to a network error"); /// /// ctor /// - public WebError(int code, string message, Exception? exception = null) : base(code, message, exception) { } + public WebError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } + } + + /// + /// Timeout error waiting for a response from the server + /// + public class TimeoutError : Error + { + /// + /// Default error info + /// + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time"); + + /// + /// ctor + /// + public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } } /// @@ -117,30 +189,14 @@ namespace CryptoExchange.Net.Objects public class DeserializeError : Error { /// - /// ctor + /// Default error info /// - public DeserializeError(string message, Exception? exception = null) : base(null, message, exception) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data"); /// /// ctor /// - protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } - } - - /// - /// Unknown error - /// - public class UnknownError : Error - { - /// - /// ctor - /// - public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { } - - /// - /// ctor - /// - protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { } + public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } } /// @@ -149,14 +205,28 @@ namespace CryptoExchange.Net.Objects public class ArgumentError : Error { /// - /// ctor + /// Default error info for missing parameter /// - public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { } + protected static readonly ErrorInfo _missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter"); + /// + /// Default error info for invalid parameter + /// + protected static readonly ErrorInfo _invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter"); /// /// ctor /// - protected ArgumentError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } + public static ArgumentError Missing(string parameterName, string? message = null) => new ArgumentError(_missingInfo with { Message = message == null ? $"{_missingInfo.Message} '{parameterName}'" : $"{_missingInfo.Message} '{parameterName}': {message}" }, null); + + /// + /// ctor + /// + public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(_invalidInfo with { Message = $"{_invalidInfo.Message} '{parameterName}': {message}" }, null); + + /// + /// ctor + /// + protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } } /// @@ -172,7 +242,7 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { } + protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { } } /// @@ -181,15 +251,19 @@ namespace CryptoExchange.Net.Objects public class ClientRateLimitError : BaseRateLimitError { /// - /// ctor + /// Default error info /// - /// - public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded"); /// /// ctor /// - protected ClientRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } + public ClientRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } + + /// + /// ctor + /// + protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { } } /// @@ -198,14 +272,19 @@ namespace CryptoExchange.Net.Objects public class ServerRateLimitError : BaseRateLimitError { /// - /// ctor + /// Default error info /// - public ServerRateLimitError(string? message = null, Exception? exception = null) : base(null, "Server rate limit exceeded" + (message?.Length > 0 ? " : " + message : null), exception) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded"); /// /// ctor /// - protected ServerRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } + public ServerRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { } + + /// + /// ctor + /// + protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { } } /// @@ -214,14 +293,19 @@ namespace CryptoExchange.Net.Objects public class CancellationRequestedError : Error { /// - /// ctor + /// Default error info /// - public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested"); /// /// ctor /// - public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } + public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { } + + /// + /// ctor + /// + protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } } /// @@ -230,13 +314,18 @@ namespace CryptoExchange.Net.Objects public class InvalidOperationError : Error { /// - /// ctor + /// Default error info /// - public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { } + protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid"); /// /// ctor /// - protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } + public InvalidOperationError(string message) : base(null, _errorInfo with { Message = message }, null) { } + + /// + /// ctor + /// + protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { } } } diff --git a/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs b/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs new file mode 100644 index 0000000..b245115 --- /dev/null +++ b/CryptoExchange.Net/Objects/Errors/ErrorEvaluator.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects.Errors +{ + /// + /// Error evaluator + /// + public class ErrorEvaluator + { + /// + /// Error code + /// + public string[] ErrorCodes { get; set; } + + /// + /// Evaluation callback for determining the error type + /// + public Func ErrorTypeEvaluator { get; set; } + + /// + /// ctor + /// + public ErrorEvaluator(string errorCode, Func errorTypeEvaluator) + { + ErrorCodes = [errorCode]; + ErrorTypeEvaluator = errorTypeEvaluator; + } + + /// + /// ctor + /// + public ErrorEvaluator(string[] errorCodes, Func errorTypeEvaluator) + { + ErrorCodes = errorCodes; + ErrorTypeEvaluator = errorTypeEvaluator; + } + } +} diff --git a/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs b/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs new file mode 100644 index 0000000..0dd38c6 --- /dev/null +++ b/CryptoExchange.Net/Objects/Errors/ErrorInfo.cs @@ -0,0 +1,58 @@ +using System; + +namespace CryptoExchange.Net.Objects.Errors +{ + /// + /// Error info + /// + public record ErrorInfo + { + /// + /// Unknown error info + /// + public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []); + + /// + /// The server error code + /// + public string[] ErrorCodes { get; set; } + /// + /// Error description + /// + public string? ErrorDescription { get; set; } + /// + /// The error type + /// + public ErrorType ErrorType { get; set; } + /// + /// Whether the error is transient and can be retried + /// + public bool IsTransient { get; set; } + /// + /// Server response message + /// + public string? Message { get; set; } + + /// + /// ctor + /// + public ErrorInfo(ErrorType errorType, string description) + { + ErrorCodes = []; + ErrorType = errorType; + IsTransient = false; + ErrorDescription = description; + } + + /// + /// ctor + /// + public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes) + { + ErrorCodes = errorCodes; + ErrorType = errorType; + IsTransient = isTransient; + ErrorDescription = description; + } + } +} diff --git a/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs b/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs new file mode 100644 index 0000000..219b3c9 --- /dev/null +++ b/CryptoExchange.Net/Objects/Errors/ErrorMapping.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net.Objects.Errors +{ + /// + /// Error mapping collection + /// + public class ErrorMapping + { + private Dictionary _evaluators = new Dictionary(); + private Dictionary _directMapping = new Dictionary(); + + /// + /// ctor + /// + public ErrorMapping(ErrorInfo[] errorMappings, ErrorEvaluator[]? errorTypeEvaluators = null) + { + foreach (var item in errorMappings) + { + if (!item.ErrorCodes.Any()) + throw new Exception("Error codes can't be null in error mapping"); + + foreach(var code in item.ErrorCodes!) + _directMapping.Add(code, item); + } + + if (errorTypeEvaluators == null) + return; + + foreach (var item in errorTypeEvaluators) + { + foreach(var code in item.ErrorCodes) + _evaluators.Add(code, item); + } + } + + /// + /// Get error info for an error code + /// + public ErrorInfo GetErrorInfo(string code, string? message) + { + if (_directMapping.TryGetValue(code!, out var info)) + return info with { Message = message }; + + if (_evaluators.TryGetValue(code!, out var eva)) + return eva.ErrorTypeEvaluator.Invoke(code!, message) with { Message = message }; + + return ErrorInfo.Unknown with { Message = message }; + } + } +} diff --git a/CryptoExchange.Net/Objects/Errors/ErrorType.cs b/CryptoExchange.Net/Objects/Errors/ErrorType.cs new file mode 100644 index 0000000..7af5017 --- /dev/null +++ b/CryptoExchange.Net/Objects/Errors/ErrorType.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects.Errors +{ + /// + /// Type of error + /// + public enum ErrorType + { + #region Library errors + + /// + /// Failed to connect to server + /// + UnableToConnect, + /// + /// Failed to complete the request to the server + /// + NetworkError, + /// + /// No API credentials have been specified + /// + MissingCredentials, + /// + /// Invalid parameter value + /// + InvalidParameter, + /// + /// Missing parameter value + /// + MissingParameter, + /// + /// Cancellation requested by user + /// + CancellationRequested, + /// + /// Invalid operation requested + /// + InvalidOperation, + /// + /// Failed to deserialize data + /// + DeserializationFailed, + /// + /// Websocket is temporarily paused + /// + WebsocketPaused, + /// + /// Timeout while waiting for data from the order book subscription + /// + OrderBookTimeout, + /// + /// All orders failed for a multi-order operation + /// + AllOrdersFailed, + /// + /// Request timeout + /// + Timeout, + + #endregion + + #region Server errors + + /// + /// Unknown error + /// + Unknown, + /// + /// Not authorized or insufficient permissions + /// + Unauthorized, + /// + /// Request rate limit error, too many requests + /// + RateLimitRequest, + /// + /// Connection rate limit error, too many connections + /// + RateLimitConnection, + /// + /// Subscription rate limit error, too many subscriptions + /// + RateLimitSubscription, + /// + /// Order rate limit error, too many orders + /// + RateLimitOrder, + /// + /// Request timestamp invalid + /// + InvalidTimestamp, + /// + /// Unknown symbol + /// + UnknownSymbol, + /// + /// Unknown asset + /// + UnknownAsset, + /// + /// Unknown order + /// + UnknownOrder, + /// + /// Duplicate subscription + /// + DuplicateSubscription, + /// + /// Invalid quantity + /// + InvalidQuantity, + /// + /// Invalid price + /// + InvalidPrice, + /// + /// Parameter(s) for stop or tp/sl order invalid + /// + InvalidStopParameters, + /// + /// Not enough balance to execute request + /// + InsufficientBalance, + /// + /// Client order id already in use + /// + DuplicateClientOrderId, + /// + /// Symbol is not currently trading + /// + UnavailableSymbol, + /// + /// Order rejected due to order configuration such as order type or time in force restrictions + /// + RejectedOrderConfiguration, + /// + /// There is no open position + /// + NoPosition, + /// + /// Max position reached + /// + MaxPosition, + /// + /// Error in the internal system + /// + SystemError, + /// + /// The target object is not in the correct state for an operation + /// + IncorrectState, + /// + /// Risk management error + /// + RiskError + + #endregion + } +} diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index d6ee7b4..cedb443 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; @@ -549,7 +550,7 @@ namespace CryptoExchange.Net.OrderBook return new CallResult(new CancellationRequestedError()); if (DateTime.UtcNow - startWait > timeout) - return new CallResult(new ServerError("Timeout while waiting for data")); + return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data"))); try { diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs index 0113ac4..8d4d552 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs @@ -58,19 +58,19 @@ namespace CryptoExchange.Net.SharedApis public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) { if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value)) - return new ArgumentError($"ApiType.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}"); + return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}"); foreach (var param in RequiredExchangeParameters) { if (!string.IsNullOrEmpty(param.Name)) { if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true) - return new ArgumentError($"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}"); + return ArgumentError.Invalid(param.Name!, $"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}"); } else { if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) - return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); + return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); } } @@ -140,12 +140,12 @@ namespace CryptoExchange.Net.SharedApis if (!string.IsNullOrEmpty(param.Name)) { if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null) - return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); + return ArgumentError.Invalid(param.Name!, $"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); } else { if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null)) - return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); + return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); } } @@ -155,10 +155,10 @@ namespace CryptoExchange.Net.SharedApis if (symbolsRequest.Symbols != null) { if (!SupportsMultipleSymbols) - return new ArgumentError($"Only a single symbol parameter is allowed, multiple symbols are not supported"); + return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported"); if (symbolsRequest.Symbols.Length > MaxSymbolCount) - return new ArgumentError($"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed"); + return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed"); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs index c567caa..d273f1a 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetClosedOrdersOptions.cs @@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (TimeFilterSupported && request.StartTime != null) - return new ArgumentError($"Time filter is not supported"); + return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs index ec30c34..873f144 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetDepositsOptions.cs @@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (TimeFilterSupported && request.StartTime != null) - return new ArgumentError($"Time filter is not supported"); + return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs index 1a1b97e..a5e496f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs @@ -67,23 +67,23 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (!IsSupported(request.Interval)) - return new ArgumentError("Interval not supported"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported"); if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return new ArgumentError($"Only the most recent {MaxAge} klines are available"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available"); if (request.Limit > MaxLimit) - return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request"); if (MaxTotalDataPoints.HasValue) { if (request.Limit > MaxTotalDataPoints.Value) - return new ArgumentError($"Only the most recent {MaxTotalDataPoints} klines are available"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available"); if (request.StartTime.HasValue == true) { if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) - return new ArgumentError($"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); + return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs index 7c117ee..a7ff8e9 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs @@ -49,13 +49,13 @@ namespace CryptoExchange.Net.SharedApis return null; if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) - return new ArgumentError($"Max limit is {MaxLimit}"); + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}"); if (MinLimit.HasValue && request.Limit.Value < MinLimit) - return new ArgumentError($"Min limit is {MaxLimit}"); + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MaxLimit}"); if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value)) - return new ArgumentError($"Limit should be one of " + string.Join(", ", SupportedLimits)); + return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Limit should be one of " + string.Join(", ", SupportedLimits)); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs index a6b43ec..815aa29 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetRecentTradesOptions.cs @@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis public Error? Validate(GetRecentTradesRequest request) { if (request.Limit > MaxLimit) - return new ArgumentError($"Only the most recent {MaxLimit} trades are available"); + return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available"); return null; } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs index 018eccb..d07972f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetTradeHistoryOptions.cs @@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) - return new ArgumentError($"Only the most recent {MaxAge} trades are available"); + return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs index 1806c6a..f6f3c7c 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetWithdrawalsOptions.cs @@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (TimeFilterSupported && request.StartTime != null) - return new ArgumentError($"Time filter is not supported"); + return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs index f89d1d3..8a7c9b1 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs @@ -36,16 +36,16 @@ namespace CryptoExchange.Net.SharedApis SharedQuantitySupport quantitySupport) { if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) - return new ArgumentError("Tp/Sl parameters not supported"); + return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.StopLossPrice) + " / " + nameof(PlaceFuturesOrderRequest.TakeProfitPrice), "Tp/Sl parameters not supported"); if (request.OrderType == SharedOrderType.Other) throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); if (!supportedOrderTypes.Contains(request.OrderType)) - return new ArgumentError("Order type not supported"); + return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.OrderType), "Order type not supported"); if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) - return new ArgumentError("Order time in force not supported"); + return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.TimeInForce), "Order time in force not supported"); var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); if (quantityError != null) diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs index 75c61b7..7e3c2e3 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs @@ -34,10 +34,10 @@ namespace CryptoExchange.Net.SharedApis throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); if (!supportedOrderTypes.Contains(request.OrderType)) - return new ArgumentError("Order type not supported"); + return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.OrderType), "Order type not supported"); if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) - return new ArgumentError("Order time in force not supported"); + return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.TimeInForce), "Order time in force not supported"); var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); if (quantityError != null) diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs index f6c4d7f..826c964 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs @@ -60,7 +60,7 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (!IsSupported(request.Interval)) - return new ArgumentError("Interval not supported"); + return ArgumentError.Invalid(nameof(SubscribeKlineRequest.Interval), "Interval not supported"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs index bd3c36b..c2baed2 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs @@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) { if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value)) - return new ArgumentError("Limit not supported"); + return ArgumentError.Invalid(nameof(SubscribeOrderBookRequest.Limit), "Limit not supported"); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); } diff --git a/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs b/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs index aea1d5a..709f441 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs @@ -84,16 +84,16 @@ namespace CryptoExchange.Net.SharedApis return null; if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null) - return new ArgumentError($"Quantity for {side}.{type} required in base or quote asset"); + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base or quote asset"); if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null) - return new ArgumentError($"Quantity for {side}.{type} required in quote asset"); + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in quote asset"); if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null) - return new ArgumentError($"Quantity for {side}.{type} required in base asset"); + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base asset"); if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null) - return new ArgumentError($"Quantity for {side}.{type} required in contracts"); + return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in contracts"); return null; } diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index a103200..56ab226 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Sockets CurrentResponses++; if (CurrentResponses == RequiredResponses) - Response = message.Data; + Response = message.Data; if (Result?.Success != false) // If an error result is already set don't override that @@ -219,7 +219,7 @@ namespace CryptoExchange.Net.Sockets return; Completed = true; - Result = new CallResult(new CancellationRequestedError(null, "Query timeout", null)); + Result = new CallResult(new TimeoutError()); ContinueAwaiter?.Set(); _event.Set(); } diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 3dc3fee..64e0f81 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -477,7 +477,7 @@ namespace CryptoExchange.Net.Sockets if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages) { - _logger.FailedToParse(SocketId, result.Error!.Message); + _logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!); return; } @@ -765,7 +765,7 @@ namespace CryptoExchange.Net.Sockets public virtual async Task SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) { await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); - return query.Result ?? new CallResult(new ServerError("Timeout")); + return query.Result ?? new CallResult(new TimeoutError()); } /// @@ -779,7 +779,7 @@ namespace CryptoExchange.Net.Sockets public virtual async Task> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) { await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); - return query.TypedResult ?? new CallResult(new ServerError("Timeout")); + return query.TypedResult ?? new CallResult(new TimeoutError()); } private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) diff --git a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs index 585a2c9..cfc4c33 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs @@ -184,7 +184,7 @@ namespace CryptoExchange.Net.Trackers.Klines if (!subResult) { - _logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception); + _logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); Status = SyncStatus.Disconnected; return subResult; } diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 0b9bf77..5d50e1e 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -207,7 +207,7 @@ namespace CryptoExchange.Net.Trackers.Trades if (!subResult) { - _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception); + _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception); Status = SyncStatus.Disconnected; return subResult; }