1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-13 17:33:02 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Jkorf f2753aed1e Updated to version 9.5.0 2025-08-19 10:21:24 +02:00
Jkorf e33d826381 Updated CryptoExchange.Net version to 9.5.0 2025-08-19 10:20:11 +02:00
Jkorf 364aa4d324 Updated to version 9.5.0 2025-08-19 10:07:07 +02:00
Jkorf 455b332757 Updated test methods 2025-08-19 10:05:14 +02:00
Jkorf 876b895645 Fixed warning 2025-08-19 10:01:49 +02:00
Jkorf daf7ed9fe6 Refactored RestApiClient authentication to prevent duplicate query string / body serialization 2025-08-19 09:50:10 +02:00
Jan Korf 3e365f83c9 Error handling update 2025-08-18 11:03:26 +02:00
Jkorf 40977ebdbe Fixed timing issue in query response processing 2025-08-07 14:15:18 +02:00
Jkorf 4a9058fc1c Fix response type in websocket queries not interested in the response 2025-08-07 14:15:00 +02:00
JKorf dab9a21608 Fixed IOrderBookSocketClient Shared interface not getting registered in DI 2025-08-04 17:12:34 +02:00
44 changed files with 775 additions and 239 deletions
@@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch (Exception ex) catch (Exception ex)
{ {
return new CallResult<object>(new DeserializeError(ex.Message)); return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -320,7 +320,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch(Exception ex) catch(Exception ex)
{ {
return new CallResult<T>(new DeserializeError(ex.ToLogString())); return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -354,7 +354,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
{ {
// Not a json message // Not a json message
IsValid = false; 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) catch (Exception ex)
{ {
return new CallResult<object>(new DeserializeError(ex.ToLogString())); return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -485,7 +485,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch (Exception ex) catch (Exception ex)
{ {
return new CallResult<T>(new DeserializeError(ex.Message)); return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -504,7 +504,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
{ {
// Not a json message // Not a json message
IsValid = false; IsValid = false;
return new CallResult(new DeserializeError("ProtobufError: " + ex.Message, ex)); return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net.Protobuf</PackageId> <PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description> <Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.4.0</PackageVersion> <PackageVersion>9.5.0</PackageVersion>
<AssemblyVersion>9.4.0</AssemblyVersion> <AssemblyVersion>9.5.0</AssemblyVersion>
<FileVersion>9.4.0</FileVersion> <FileVersion>9.5.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags> <PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile> <DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.4.0" /> <PackageReference Include="CryptoExchange.Net" Version="9.5.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" /> <PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3
View File
@@ -5,6 +5,9 @@
Protobuf support for CryptoExchange.Net. Protobuf support for CryptoExchange.Net.
## Release notes ## Release notes
* Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0
* Version 9.4.0 - 04 Aug 2025 * Version 9.4.0 - 04 Aug 2025
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/ * Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Updated protobuf-net package version to 3.2.56 * Updated protobuf-net package version to 3.2.56
+15 -14
View File
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System; using System;
@@ -16,9 +17,9 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestBasicErrorCallResult() 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);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
} }
@@ -36,9 +37,9 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestCallResultError() public void TestCallResultError()
{ {
var result = new CallResult<object>(new ServerError("TestError")); var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.Message, "TestError"); ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data); ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
@@ -71,11 +72,11 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestCallResultErrorAs() public void TestCallResultErrorAs()
{ {
var result = new CallResult<TestObjectResult>(new ServerError("TestError")); var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.As<TestObject2>(default); var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError"); ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@@ -84,11 +85,11 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestCallResultErrorAsError() public void TestCallResultErrorAsError()
{ {
var result = new CallResult<TestObjectResult>(new ServerError("TestError")); var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@@ -97,11 +98,11 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestWebCallResultErrorAsError() public void TestWebCallResultErrorAsError()
{ {
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError")); var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@@ -124,10 +125,10 @@ namespace CryptoExchange.Net.UnitTests
ResultDataSource.Server, ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); 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.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api"); Assert.That(asResult.RequestUrl == "https://test.com/api");
@@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null); Assert.That(result.Error != null);
Assert.That(result.Error is ServerError); 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"); Assert.That(result.Error.Message == "Invalid request");
} }
@@ -182,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test1", true)] [TestCase("/sapi/test1", true)]
[TestCase("/sapi/test2", true)] [TestCase("/sapi/test2", true)]
[TestCase("/api/test1", false)] [TestCase("/api/test1", false)]
[TestCase("sapi/test1", false)] [TestCase("sapi/test1", true)]
[TestCase("/sapi/", true)] [TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting) public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{ {
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using System; using System;
@@ -40,7 +41,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{ {
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase)) if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{ {
return new CallResult<SubResponse>(new ServerError(message.Data.Status)); return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
} }
return message.ToCallResult(); return message.ToCallResult();
@@ -10,6 +10,7 @@ using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
@@ -55,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
var accessor = CreateAccessor(); var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result; var valid = accessor.Read(stream, true).Result;
if (!valid) if (!valid)
return new CallResult<T>(new ServerError(data)); return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
return deserializeResult; return deserializeResult;
@@ -77,10 +78,10 @@ namespace CryptoExchange.Net.UnitTests
{ {
} }
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat) public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{ {
} }
public string GetKey() => _credentials.Key; public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret; public string GetSecret() => _credentials.Secret;
} }
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Options;
using System.Linq; using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CryptoExchange.Net.Objects.Errors;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -197,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
var errorData = accessor.Deserialize<TestError>(); var errorData = accessor.Deserialize<TestError>();
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() public override TimeSpan? GetTimeOffset()
@@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication
} }
/// <summary> /// <summary>
/// Authenticate a request. Output parameters should include the providedParameters input /// Authenticate a request
/// </summary> /// </summary>
/// <param name="apiClient">The Api client sending the request</param> /// <param name="apiClient">The Api client sending the request</param>
/// <param name="uri">The uri for the request</param> /// <param name="requestConfig">The request configuration</param>
/// <param name="method">The method of the request</param> public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="requestBodyFormat">The formatting of the request body</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
public abstract void AuthenticateRequest(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
ref IDictionary<string, object>? uriParameters,
ref IDictionary<string, object>? bodyParameters,
ref Dictionary<string, string>? headers,
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
RequestBodyFormat requestBodyFormat
);
/// <summary> /// <summary>
/// SHA256 sign the data and return the bytes /// SHA256 sign the data and return the bytes
@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -54,6 +56,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public ExchangeOptions ClientOptions { get; } public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -87,6 +94,16 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc /> /// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc /> /// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{ {
+48 -59
View File
@@ -11,6 +11,7 @@ using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
@@ -54,7 +55,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Request headers to be sent with each request /// Request headers to be sent with each request
/// </summary> /// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; } protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary> /// <summary>
/// Whether parameters need to be ordered /// Whether parameters need to be ordered
@@ -363,74 +364,58 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders) Dictionary<string, string>? additionalHeaders)
{ {
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters); var requestConfiguration = new RestRequestConfiguration(
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters); definition,
baseAddress,
uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
new Dictionary<string, string>(additionalHeaders ?? []),
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
var uri = new Uri(baseAddress.AppendPath(definition.Path)); try
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
Dictionary<string, string>? headers = null;
if (AuthenticationProvider != null)
{ {
try AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
definition.Method,
ref uriParams,
ref bodyParams,
ref headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat
);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
} }
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
var queryString = requestConfiguration.GetQueryString(true);
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
queryString = $"?{queryString}";
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
if (uriParams != null)
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId); var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
if (headers != null) foreach (var header in requestConfiguration.Headers)
{ request.AddHeader(header.Key, header.Value);
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null) foreach (var header in StandardRequestHeaders)
{ {
foreach (var header in additionalHeaders) // Only add it if it isn't overwritten
if (!requestConfiguration.Headers.ContainsKey(header.Key))
request.AddHeader(header.Key, header.Value); request.AddHeader(header.Key, header.Value);
} }
if (StandardRequestHeaders != null) if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
{ {
foreach (var header in StandardRequestHeaders) var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
var bodyContent = requestConfiguration.GetBodyContent();
if (bodyContent != null)
{ {
// Only add it if it isn't overwritten request.SetContent(bodyContent, contentType);
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
} }
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParams != null && bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); {
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
} }
return request; return request;
@@ -485,8 +470,10 @@ namespace CryptoExchange.Net.Clients
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); 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) if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode; error.Code = (int)response.StatusCode;
#pragma warning restore CS0618 // Type or member is obsolete
return new WebCallResult<T>(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<T>(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 +486,7 @@ namespace CryptoExchange.Net.Clients
if (!valid) if (!valid)
{ {
// Invalid json // Invalid json
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception); return new WebCallResult<T>(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);
return new WebCallResult<T>(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);
} }
// Json response received // Json response received
@@ -526,7 +512,8 @@ namespace CryptoExchange.Net.Clients
catch (HttpRequestException requestException) catch (HttpRequestException requestException)
{ {
// Request exception, can't reach server for instance // Request exception, can't reach server for instance
return new WebCallResult<T>(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<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
catch (OperationCanceledException canceledException) catch (OperationCanceledException canceledException)
{ {
@@ -538,7 +525,9 @@ namespace CryptoExchange.Net.Clients
else else
{ {
// Request timed out // Request timed out
return new WebCallResult<T>(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<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
} }
finally finally
@@ -633,7 +622,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception) protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{ {
return new ServerError(null, "Unknown request error", exception); return new ServerError(ErrorInfo.Unknown, exception);
} }
/// <summary> /// <summary>
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
@@ -265,7 +266,7 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused")); return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
var waitEvent = new AsyncResetEvent(false); var waitEvent = new AsyncResetEvent(false);
@@ -368,7 +369,7 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<THandlerResponse>(new ServerError("Socket is paused")); return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
@@ -60,13 +60,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
catch (JsonException ex) 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<object>(new DeserializeError(info, ex)); return new CallResult<object>(new DeserializeError(info, ex));
} }
catch (Exception ex) catch (Exception ex)
{ {
var info = $"Deserialize unknown Exception: {ex.Message}"; return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
return new CallResult<object>(new DeserializeError(info, ex));
} }
} }
@@ -87,13 +86,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
catch (JsonException ex) 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<T>(new DeserializeError(info, ex)); return new CallResult<T>(new DeserializeError(info, ex));
} }
catch (Exception ex) catch (Exception ex)
{ {
var info = $"Unknown exception: {ex.Message}"; return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
return new CallResult<T>(new DeserializeError(info, ex));
} }
} }
@@ -286,7 +284,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
// Not a json message // Not a json message
IsValid = false; 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 // Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false; IsValid = false;
return new CallResult(new ServerError("Not a json value")); return new CallResult(new DeserializeError("Not a json value"));
} }
_document = JsonDocument.Parse(data); _document = JsonDocument.Parse(data);
@@ -349,7 +347,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
// Not a json message // Not a json message
IsValid = false; IsValid = false;
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
} }
} }
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.4.0</PackageVersion> <PackageVersion>9.5.0</PackageVersion>
<AssemblyVersion>9.4.0</AssemblyVersion> <AssemblyVersion>9.5.0</AssemblyVersion>
<FileVersion>9.4.0</FileVersion> <FileVersion>9.5.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
+2 -2
View File
@@ -498,8 +498,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IBookTickerSocketClient)client(x)!); services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T))) if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineSocketClient)client(x)!); services.AddTransient(x => (IKlineSocketClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T))) if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!); services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T))) if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickerSocketClient)client(x)!); services.AddTransient(x => (ITickerSocketClient)client(x)!);
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T))) if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
+145 -56
View File
@@ -1,4 +1,5 @@
using System; using CryptoExchange.Net.Objects.Errors;
using System;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
{ {
@@ -7,15 +8,50 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public abstract class Error public abstract class Error
{ {
private int? _code;
/// <summary> /// <summary>
/// The error code from the server /// The error code from the server
/// </summary> /// </summary>
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;
}
}
/// <summary> /// <summary>
/// The message for the error that occurred /// The error code returned by the server
/// </summary> /// </summary>
public string Message { get; set; } public string? ErrorCode { get; set; }
/// <summary>
/// The error description
/// </summary>
public string? ErrorDescription { get; set; }
/// <summary>
/// Error type
/// </summary>
public ErrorType ErrorType { get; set; }
/// <summary>
/// Whether the error is transient and can be retried
/// </summary>
public bool IsTransient { get; set; }
/// <summary>
/// The server message for the error that occurred
/// </summary>
public string? Message { get; set; }
/// <summary> /// <summary>
/// Underlying exception /// Underlying exception
@@ -25,10 +61,13 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected Error (int? code, string message, Exception? exception) protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception)
{ {
Code = code; ErrorCode = errorCode;
Message = message; ErrorType = errorInfo.ErrorType;
Message = errorInfo.Message;
ErrorDescription = errorInfo.ErrorDescription;
IsTransient = errorInfo.IsTransient;
Exception = exception; Exception = exception;
} }
@@ -38,7 +77,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public override string ToString() 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 public class CantConnectError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
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");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { } public CantConnectError() : base(null, _errorInfo, null) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { } public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -69,14 +113,19 @@ namespace CryptoExchange.Net.Objects
public class NoApiCredentialsError : Error public class NoApiCredentialsError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
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");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { } public NoApiCredentialsError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -87,12 +136,19 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public ServerError(string message) : base(null, message, null) { } public ServerError(ErrorInfo errorInfo, Exception? exception = null)
: base(null, errorInfo, exception) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
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) { }
/// <summary>
/// ctor
/// </summary>
public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { }
} }
/// <summary> /// <summary>
@@ -101,14 +157,30 @@ namespace CryptoExchange.Net.Objects
public class WebError : Error public class WebError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
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");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
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) { }
}
/// <summary>
/// Timeout error waiting for a response from the server
/// </summary>
public class TimeoutError : Error
{
/// <summary>
/// Default error info
/// </summary>
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time");
/// <summary>
/// ctor
/// </summary>
public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
} }
/// <summary> /// <summary>
@@ -117,30 +189,14 @@ namespace CryptoExchange.Net.Objects
public class DeserializeError : Error public class DeserializeError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
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");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected DeserializeError(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) { }
}
/// <summary>
/// Unknown error
/// </summary>
public class UnknownError : Error
{
/// <summary>
/// ctor
/// </summary>
public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { }
/// <summary>
/// ctor
/// </summary>
protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { }
} }
/// <summary> /// <summary>
@@ -149,14 +205,28 @@ namespace CryptoExchange.Net.Objects
public class ArgumentError : Error public class ArgumentError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info for missing parameter
/// </summary> /// </summary>
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { } protected static readonly ErrorInfo _missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter");
/// <summary>
/// Default error info for invalid parameter
/// </summary>
protected static readonly ErrorInfo _invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
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);
/// <summary>
/// ctor
/// </summary>
public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(_invalidInfo with { Message = $"{_invalidInfo.Message} '{parameterName}': {message}" }, null);
/// <summary>
/// ctor
/// </summary>
protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -172,7 +242,7 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { } protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
} }
/// <summary> /// <summary>
@@ -181,15 +251,19 @@ namespace CryptoExchange.Net.Objects
public class ClientRateLimitError : BaseRateLimitError public class ClientRateLimitError : BaseRateLimitError
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
/// <param name="message"></param> protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded");
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
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) { }
/// <summary>
/// ctor
/// </summary>
protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
} }
/// <summary> /// <summary>
@@ -198,14 +272,19 @@ namespace CryptoExchange.Net.Objects
public class ServerRateLimitError : BaseRateLimitError public class ServerRateLimitError : BaseRateLimitError
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
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");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
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) { }
/// <summary>
/// ctor
/// </summary>
protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
} }
/// <summary> /// <summary>
@@ -214,14 +293,19 @@ namespace CryptoExchange.Net.Objects
public class CancellationRequestedError : Error public class CancellationRequestedError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -230,13 +314,18 @@ namespace CryptoExchange.Net.Objects
public class InvalidOperationError : Error public class InvalidOperationError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public InvalidOperationError(string message) : base(null, _errorInfo with { Message = message }, null) { }
/// <summary>
/// ctor
/// </summary>
protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
} }
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error evaluator
/// </summary>
public class ErrorEvaluator
{
/// <summary>
/// Error code
/// </summary>
public string[] ErrorCodes { get; set; }
/// <summary>
/// Evaluation callback for determining the error type
/// </summary>
public Func<string, string?, ErrorInfo> ErrorTypeEvaluator { get; set; }
/// <summary>
/// ctor
/// </summary>
public ErrorEvaluator(string errorCode, Func<string, string?, ErrorInfo> errorTypeEvaluator)
{
ErrorCodes = [errorCode];
ErrorTypeEvaluator = errorTypeEvaluator;
}
/// <summary>
/// ctor
/// </summary>
public ErrorEvaluator(string[] errorCodes, Func<string, string?, ErrorInfo> errorTypeEvaluator)
{
ErrorCodes = errorCodes;
ErrorTypeEvaluator = errorTypeEvaluator;
}
}
}
@@ -0,0 +1,58 @@
using System;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error info
/// </summary>
public record ErrorInfo
{
/// <summary>
/// Unknown error info
/// </summary>
public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []);
/// <summary>
/// The server error code
/// </summary>
public string[] ErrorCodes { get; set; }
/// <summary>
/// Error description
/// </summary>
public string? ErrorDescription { get; set; }
/// <summary>
/// The error type
/// </summary>
public ErrorType ErrorType { get; set; }
/// <summary>
/// Whether the error is transient and can be retried
/// </summary>
public bool IsTransient { get; set; }
/// <summary>
/// Server response message
/// </summary>
public string? Message { get; set; }
/// <summary>
/// ctor
/// </summary>
public ErrorInfo(ErrorType errorType, string description)
{
ErrorCodes = [];
ErrorType = errorType;
IsTransient = false;
ErrorDescription = description;
}
/// <summary>
/// ctor
/// </summary>
public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes)
{
ErrorCodes = errorCodes;
ErrorType = errorType;
IsTransient = isTransient;
ErrorDescription = description;
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error mapping collection
/// </summary>
public class ErrorMapping
{
private Dictionary<string, ErrorEvaluator> _evaluators = new Dictionary<string, ErrorEvaluator>();
private Dictionary<string, ErrorInfo> _directMapping = new Dictionary<string, ErrorInfo>();
/// <summary>
/// ctor
/// </summary>
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);
}
}
/// <summary>
/// Get error info for an error code
/// </summary>
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 };
}
}
}
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Type of error
/// </summary>
public enum ErrorType
{
#region Library errors
/// <summary>
/// Failed to connect to server
/// </summary>
UnableToConnect,
/// <summary>
/// Failed to complete the request to the server
/// </summary>
NetworkError,
/// <summary>
/// No API credentials have been specified
/// </summary>
MissingCredentials,
/// <summary>
/// Invalid parameter value
/// </summary>
InvalidParameter,
/// <summary>
/// Missing parameter value
/// </summary>
MissingParameter,
/// <summary>
/// Cancellation requested by user
/// </summary>
CancellationRequested,
/// <summary>
/// Invalid operation requested
/// </summary>
InvalidOperation,
/// <summary>
/// Failed to deserialize data
/// </summary>
DeserializationFailed,
/// <summary>
/// Websocket is temporarily paused
/// </summary>
WebsocketPaused,
/// <summary>
/// Timeout while waiting for data from the order book subscription
/// </summary>
OrderBookTimeout,
/// <summary>
/// All orders failed for a multi-order operation
/// </summary>
AllOrdersFailed,
/// <summary>
/// Request timeout
/// </summary>
Timeout,
#endregion
#region Server errors
/// <summary>
/// Unknown error
/// </summary>
Unknown,
/// <summary>
/// Not authorized or insufficient permissions
/// </summary>
Unauthorized,
/// <summary>
/// Request rate limit error, too many requests
/// </summary>
RateLimitRequest,
/// <summary>
/// Connection rate limit error, too many connections
/// </summary>
RateLimitConnection,
/// <summary>
/// Subscription rate limit error, too many subscriptions
/// </summary>
RateLimitSubscription,
/// <summary>
/// Order rate limit error, too many orders
/// </summary>
RateLimitOrder,
/// <summary>
/// Request timestamp invalid
/// </summary>
InvalidTimestamp,
/// <summary>
/// Unknown symbol
/// </summary>
UnknownSymbol,
/// <summary>
/// Unknown asset
/// </summary>
UnknownAsset,
/// <summary>
/// Unknown order
/// </summary>
UnknownOrder,
/// <summary>
/// Duplicate subscription
/// </summary>
DuplicateSubscription,
/// <summary>
/// Invalid quantity
/// </summary>
InvalidQuantity,
/// <summary>
/// Invalid price
/// </summary>
InvalidPrice,
/// <summary>
/// Parameter(s) for stop or tp/sl order invalid
/// </summary>
InvalidStopParameters,
/// <summary>
/// Not enough balance to execute request
/// </summary>
InsufficientBalance,
/// <summary>
/// Client order id already in use
/// </summary>
DuplicateClientOrderId,
/// <summary>
/// Symbol is not currently trading
/// </summary>
UnavailableSymbol,
/// <summary>
/// Order rejected due to order configuration such as order type or time in force restrictions
/// </summary>
RejectedOrderConfiguration,
/// <summary>
/// There is no open position
/// </summary>
NoPosition,
/// <summary>
/// Max position reached
/// </summary>
MaxPosition,
/// <summary>
/// Error in the internal system
/// </summary>
SystemError,
/// <summary>
/// The target object is not in the correct state for an operation
/// </summary>
IncorrectState,
/// <summary>
/// Risk management error
/// </summary>
RiskError
#endregion
}
}
@@ -76,6 +76,9 @@ namespace CryptoExchange.Net.Objects
{ {
Path = path; Path = path;
Method = method; Method = method;
if (!Path.StartsWith("/"))
Path = $"/{Path}";
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Rest request configuration
/// </summary>
public class RestRequestConfiguration
{
private string? _bodyContent;
private string? _queryString;
/// <summary>
/// Http method
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Whether the request needs authentication
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Base address for the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The request path
/// </summary>
public string Path { get; set; }
/// <summary>
/// Query parameters
/// </summary>
public IDictionary<string, object> QueryParameters { get; set; }
/// <summary>
/// Body parameters
/// </summary>
public IDictionary<string, object> BodyParameters { get; set; }
/// <summary>
/// Request headers
/// </summary>
public IDictionary<string, string> Headers { get; set; }
/// <summary>
/// Array serialization type
/// </summary>
public ArrayParametersSerialization ArraySerialization { get; set; }
/// <summary>
/// Position of the parameters
/// </summary>
public HttpMethodParameterPosition ParameterPosition { get; set; }
/// <summary>
/// Body format
/// </summary>
public RequestBodyFormat BodyFormat { get; set; }
/// <summary>
/// ctor
/// </summary>
public RestRequestConfiguration(
RequestDefinition requestDefinition,
string baseAddress,
IDictionary<string, object> queryParams,
IDictionary<string, object> bodyParams,
IDictionary<string, string> headers,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parametersPosition,
RequestBodyFormat bodyFormat)
{
Method = requestDefinition.Method;
Authenticated = requestDefinition.Authenticated;
Path = requestDefinition.Path;
BaseAddress = baseAddress;
QueryParameters = queryParams;
BodyParameters = bodyParams;
Headers = headers;
ArraySerialization = arraySerialization;
ParameterPosition = parametersPosition;
BodyFormat = bodyFormat;
}
/// <summary>
/// Get the parameter collection based on the ParameterPosition
/// </summary>
public IDictionary<string, object> GetPositionParameters()
{
if (ParameterPosition == HttpMethodParameterPosition.InBody)
return BodyParameters;
return QueryParameters;
}
/// <summary>
/// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that.
/// </summary>
/// <param name="urlEncode">Whether to URL encode the parameter string if creating new</param>
public string GetQueryString(bool urlEncode = true)
{
return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization);
}
/// <summary>
/// Set the query string of the request. Will be returned by subsequent <see cref="GetQueryString" /> calls
/// </summary>
public void SetQueryString(string value)
{
_queryString = value;
}
/// <summary>
/// Get the body content if it's previously set
/// </summary>
public string? GetBodyContent()
{
return _bodyContent;
}
/// <summary>
/// Set the body content for the request
/// </summary>
public void SetBodyContent(string content)
{
_bodyContent = content;
}
}
}
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -549,7 +550,7 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(new CancellationRequestedError()); return new CallResult<bool>(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > timeout) if (DateTime.UtcNow - startWait > timeout)
return new CallResult<bool>(new ServerError("Timeout while waiting for data")); return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try try
{ {
@@ -58,19 +58,19 @@ namespace CryptoExchange.Net.SharedApis
public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
{ {
if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value)) 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) foreach (var param in RequiredExchangeParameters)
{ {
if (!string.IsNullOrEmpty(param.Name)) if (!string.IsNullOrEmpty(param.Name))
{ {
if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true) 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 else
{ {
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) 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 (!string.IsNullOrEmpty(param.Name))
{ {
if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null) 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 else
{ {
if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null)) 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 (symbolsRequest.Symbols != null)
{ {
if (!SupportsMultipleSymbols) 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) 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");
} }
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (TimeFilterSupported && request.StartTime != null) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (TimeFilterSupported && request.StartTime != null) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -67,23 +67,23 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (!IsSupported(request.Interval)) 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)) 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) 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 (MaxTotalDataPoints.HasValue)
{ {
if (request.Limit > MaxTotalDataPoints.Value) 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.StartTime.HasValue == true)
{ {
if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) 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");
} }
} }
@@ -49,13 +49,13 @@ namespace CryptoExchange.Net.SharedApis
return null; return null;
if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) 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) 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)) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public Error? Validate(GetRecentTradesRequest request) public Error? Validate(GetRecentTradesRequest request)
{ {
if (request.Limit > MaxLimit) 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; return null;
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (TimeFilterSupported && request.StartTime != null) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -36,16 +36,16 @@ namespace CryptoExchange.Net.SharedApis
SharedQuantitySupport quantitySupport) SharedQuantitySupport quantitySupport)
{ {
if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) 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) if (request.OrderType == SharedOrderType.Other)
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
if (!supportedOrderTypes.Contains(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)) 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); var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
if (quantityError != null) if (quantityError != null)
@@ -34,10 +34,10 @@ namespace CryptoExchange.Net.SharedApis
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
if (!supportedOrderTypes.Contains(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)) 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); var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
if (quantityError != null) if (quantityError != null)
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (!IsSupported(request.Interval)) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value)) 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); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -84,16 +84,16 @@ namespace CryptoExchange.Net.SharedApis
return null; return null;
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == 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) 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) 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) 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; return null;
} }
+1 -1
View File
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public static MessageMatcher Create<T>(string value) public static MessageMatcher Create<T>(string value)
{ {
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, (con, msg) => CallResult.SuccessResult)); return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, (con, msg) => new CallResult<T>(default, msg.OriginalData, null)));
} }
/// <summary> /// <summary>
+3 -5
View File
@@ -187,11 +187,8 @@ namespace CryptoExchange.Net.Sockets
return CallResult.SuccessResult; return CallResult.SuccessResult;
CurrentResponses++; CurrentResponses++;
if (CurrentResponses == RequiredResponses) if (CurrentResponses == RequiredResponses)
{
Completed = true;
Response = message.Data; Response = message.Data;
}
if (Result?.Success != false) if (Result?.Success != false)
// If an error result is already set don't override that // If an error result is already set don't override that
@@ -199,6 +196,7 @@ namespace CryptoExchange.Net.Sockets
if (CurrentResponses == RequiredResponses) if (CurrentResponses == RequiredResponses)
{ {
Completed = true;
_event.Set(); _event.Set();
if (ContinueAwaiter != null) if (ContinueAwaiter != null)
await ContinueAwaiter.WaitAsync().ConfigureAwait(false); await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
@@ -221,7 +219,7 @@ namespace CryptoExchange.Net.Sockets
return; return;
Completed = true; Completed = true;
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null)); Result = new CallResult<THandlerResponse>(new TimeoutError());
ContinueAwaiter?.Set(); ContinueAwaiter?.Set();
_event.Set(); _event.Set();
} }
@@ -477,7 +477,7 @@ namespace CryptoExchange.Net.Sockets
if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages) if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages)
{ {
_logger.FailedToParse(SocketId, result.Error!.Message); _logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!);
return; return;
} }
@@ -765,7 +765,7 @@ namespace CryptoExchange.Net.Sockets
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
{ {
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.Result ?? new CallResult(new ServerError("Timeout")); return query.Result ?? new CallResult(new TimeoutError());
} }
/// <summary> /// <summary>
@@ -779,7 +779,7 @@ namespace CryptoExchange.Net.Sockets
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<THandlerResponse>(Query<THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<THandlerResponse>(Query<THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
{ {
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout")); return query.TypedResult ?? new CallResult<THandlerResponse>(new TimeoutError());
} }
private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default)
+15 -11
View File
@@ -131,17 +131,21 @@ namespace CryptoExchange.Net.Testing
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc)); authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
authProvider.AuthenticateRequest( authProvider.ProcessRequest(
client, client,
new Uri(host.AppendPath(path)), new RestRequestConfiguration(
method, new RequestDefinition(path, method)
ref uriParams, {
ref bodyParams, Authenticated = true
ref headers, },
true, host,
client.ArraySerialization, uriParams ?? new Dictionary<string, object>(),
client.ParameterPositions[method], bodyParams ?? new Dictionary<string, object>(),
client.RequestBodyFormat headers,
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
)
); );
var signature = getSignature(uriParams, bodyParams, headers); var signature = getSignature(uriParams, bodyParams, headers);
@@ -184,7 +184,7 @@ namespace CryptoExchange.Net.Trackers.Klines
if (!subResult) 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; Status = SyncStatus.Disconnected;
return subResult; return subResult;
} }
@@ -207,7 +207,7 @@ namespace CryptoExchange.Net.Trackers.Trades
if (!subResult) 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; Status = SyncStatus.Disconnected;
return subResult; return subResult;
} }
+1 -1
View File
@@ -1,4 +1,4 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found> </Found>
+10
View File
@@ -59,6 +59,16 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 9.5.0 - 19 Aug 2025
* Added better error handling support
* Added ErrorDescription, ErrorType and IsTransient to Error object
* Added ErrorCode in favor of Code
* Updated some error messages
* Refactored RestApiClient request authentication and AuthenticationProvider to prevent duplicate query string / body serialization
* Fixed IOrderBookSocketClient Shared interface not getting registered in DI
* Fixed response type in websocket queries not interested in the response
* Fixed timing issue in query response processing
* Version 9.4.0 - 04 Aug 2025 * Version 9.4.0 - 04 Aug 2025
* Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported * Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported