1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 16:32:57 +00:00

Compare commits

...

20 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
Jkorf a89c222399 Updated to version 9.4.0 2025-08-04 09:56:45 +02:00
Jkorf 1e356d2a45 Updated CryptoExchange.Net and protobuf-net package versions for CryptoExchange.Net.Protobuf 2025-08-04 09:54:15 +02:00
Jkorf eed794c2cf Updated to version 9.4.0 2025-08-04 09:47:36 +02:00
Jkorf 2f82e2015b Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported 2025-08-04 09:39:30 +02:00
Jkorf ad599badb2 Added CryptoExchange.Net tag 2025-07-29 16:02:38 +02:00
Jkorf 1e45c73f1d Added CoinW to examples 2025-07-29 15:57:02 +02:00
Jkorf 49c1fda2c1 Added CoinW reference 2025-07-29 11:44:22 +02:00
Jkorf 32a31e464b Updated to version 9.3.1 2025-07-29 09:42:53 +02:00
Jkorf cddb4167e4 Added Id property to SharedPosition model 2025-07-28 15:58:36 +02:00
Jkorf 65457d8df2 Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum 2025-07-28 15:58:19 +02:00
62 changed files with 1011 additions and 276 deletions
@@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
}
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)
{
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
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<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)
{
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
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>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.3.0</PackageVersion>
<AssemblyVersion>9.3.0</AssemblyVersion>
<FileVersion>9.3.0</FileVersion>
<PackageVersion>9.5.0</PackageVersion>
<AssemblyVersion>9.5.0</AssemblyVersion>
<FileVersion>9.5.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.3.0" />
<PackageReference Include="protobuf-net" Version="3.2.52" />
<PackageReference Include="CryptoExchange.Net" Version="9.5.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
+7
View File
@@ -5,6 +5,13 @@
Protobuf support for CryptoExchange.Net.
## Release notes
* Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0
* Version 9.4.0 - 04 Aug 2025
* 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
* Version 9.3.0 - 23 Jul 2025
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
+15 -14
View File
@@ -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<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.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
@@ -71,11 +72,11 @@ namespace CryptoExchange.Net.UnitTests
[Test]
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);
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<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(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<TestObjectResult>(new ServerError("TestError"));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(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<TestObject2>(new ServerError("TestError2"));
var asResult = result.AsError<TestObject2>(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");
@@ -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");
}
@@ -182,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test1", true)]
[TestCase("/sapi/test2", true)]
[TestCase("/api/test1", false)]
[TestCase("sapi/test1", false)]
[TestCase("sapi/test1", true)]
[TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{
@@ -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<SubResponse>(new ServerError(message.Data.Status));
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
}
return message.ToCallResult();
@@ -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<T>(new ServerError(data));
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>();
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 GetSecret() => _credentials.Secret;
}
@@ -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<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()
@@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication
}
/// <summary>
/// Authenticate a request. Output parameters should include the providedParameters input
/// Authenticate a request
/// </summary>
/// <param name="apiClient">The Api client sending the request</param>
/// <param name="uri">The uri for the request</param>
/// <param name="method">The method of the request</param>
/// <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
);
/// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// SHA256 sign the data and return the bytes
@@ -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
/// </summary>
public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// ctor
/// </summary>
@@ -87,6 +94,16 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
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 />
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.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;
@@ -54,7 +55,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary>
/// Whether parameters need to be ordered
@@ -363,74 +364,58 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders)
{
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
var requestConfiguration = new RestRequestConfiguration(
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));
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
{
try
{
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);
}
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
}
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
if (uriParams != null)
uri = uri.SetParameters(uriParams, arraySerialization);
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
if (headers != null)
{
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
foreach (var header in requestConfiguration.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);
}
}
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
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
request.SetContent(bodyContent, contentType);
}
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParams != null && bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
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;
@@ -485,8 +470,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<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)
{
// 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, 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, valid.Error);
}
// Json response received
@@ -526,7 +512,8 @@ namespace CryptoExchange.Net.Clients
catch (HttpRequestException requestException)
{
// 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)
{
@@ -538,7 +525,9 @@ namespace CryptoExchange.Net.Clients
else
{
// 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
@@ -633,7 +622,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
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>
@@ -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<UpdateSubscription>(new ServerError("Socket is paused"));
return new CallResult<UpdateSubscription>(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<THandlerResponse>(new ServerError("Socket is paused"));
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
if (ct.IsCancellationRequested)
@@ -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<object>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
var info = $"Deserialize unknown Exception: {ex.Message}";
return new CallResult<object>(new DeserializeError(info, ex));
return new CallResult<object>(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<T>(new DeserializeError(info, ex));
}
catch (Exception ex)
{
var info = $"Unknown exception: {ex.Message}";
return new CallResult<T>(new DeserializeError(info, ex));
return new CallResult<T>(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));
}
}
+4 -4
View File
@@ -6,11 +6,11 @@
<PackageId>CryptoExchange.Net</PackageId>
<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>
<PackageVersion>9.3.0</PackageVersion>
<AssemblyVersion>9.3.0</AssemblyVersion>
<FileVersion>9.3.0</FileVersion>
<PackageVersion>9.5.0</PackageVersion>
<AssemblyVersion>9.5.0</AssemblyVersion>
<FileVersion>9.5.0</FileVersion>
<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</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>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
+2 -2
View File
@@ -23,14 +23,14 @@ namespace CryptoExchange.Net
{
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
{
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
_symbolInfos.TryAdd(topicId, exchangeInfo);
}
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name }));
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
}
/// <summary>
+2 -2
View File
@@ -498,8 +498,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineSocketClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickerSocketClient)client(x)!);
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
{
@@ -7,15 +8,50 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public abstract class Error
{
private int? _code;
/// <summary>
/// The error code from the server
/// </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>
/// The message for the error that occurred
/// The error code returned by the server
/// </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>
/// Underlying exception
@@ -25,10 +61,13 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
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
/// <returns></returns>
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
{
/// <summary>
/// ctor
/// Default error info
/// </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>
/// ctor
/// </summary>
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { }
public CantConnectError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </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>
@@ -69,14 +113,19 @@ namespace CryptoExchange.Net.Objects
public class NoApiCredentialsError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </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>
/// ctor
/// </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>
@@ -87,12 +136,19 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
public ServerError(string message) : base(null, message, null) { }
public ServerError(ErrorInfo errorInfo, Exception? exception = null)
: base(null, errorInfo, exception) { }
/// <summary>
/// ctor
/// </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>
@@ -101,14 +157,30 @@ namespace CryptoExchange.Net.Objects
public class WebError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </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>
/// ctor
/// </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>
@@ -117,30 +189,14 @@ namespace CryptoExchange.Net.Objects
public class DeserializeError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </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>
/// ctor
/// </summary>
protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, 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) { }
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
@@ -149,14 +205,28 @@ namespace CryptoExchange.Net.Objects
public class ArgumentError : Error
{
/// <summary>
/// ctor
/// Default error info for missing parameter
/// </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>
/// ctor
/// </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>
@@ -172,7 +242,7 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// ctor
/// </summary>
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { }
protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
}
/// <summary>
@@ -181,15 +251,19 @@ namespace CryptoExchange.Net.Objects
public class ClientRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
/// <param name="message"></param>
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");
/// <summary>
/// ctor
/// </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>
@@ -198,14 +272,19 @@ namespace CryptoExchange.Net.Objects
public class ServerRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// Default error info
/// </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>
/// ctor
/// </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>
@@ -214,14 +293,19 @@ namespace CryptoExchange.Net.Objects
public class CancellationRequestedError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </summary>
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { }
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
/// <summary>
/// ctor
/// </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>
@@ -230,13 +314,18 @@ namespace CryptoExchange.Net.Objects
public class InvalidOperationError : Error
{
/// <summary>
/// ctor
/// Default error info
/// </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>
/// ctor
/// </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;
Method = method;
if (!Path.StartsWith("/"))
Path = $"/{Path}";
}
/// <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.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<bool>(new CancellationRequestedError());
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
{
@@ -20,6 +20,10 @@
/// <summary>
/// Quantity can be either base or quote quantity
/// </summary>
BaseAndQuoteAsset
BaseAndQuoteAsset,
/// <summary>
/// Quantity can be either base or quote quantity, or in contracts
/// </summary>
BaseAndQuoteAssetAndContracts
}
}
@@ -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}");
}
}
@@ -109,6 +109,15 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public List<ParameterDescription> RequiredOptionalParameters { get; set; } = new List<ParameterDescription>();
/// <summary>
/// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters)
/// </summary>
public bool SupportsMultipleSymbols { get; set; } = false;
/// <summary>
/// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters)
/// </summary>
public int? MaxSymbolCount { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -131,12 +140,25 @@ 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}");
}
}
if (request is SharedSymbolRequest symbolsRequest)
{
if (symbolsRequest.Symbols != null)
{
if (!SupportsMultipleSymbols)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported");
if (symbolsRequest.Symbols.Length > MaxSymbolCount)
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)
{
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);
}
@@ -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);
}
@@ -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");
}
}
@@ -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);
}
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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)
@@ -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)
@@ -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);
}
@@ -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);
}
@@ -65,6 +65,9 @@ namespace CryptoExchange.Net.SharedApis
if (supportedType == quantityType)
return true;
if (supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
return true;
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset))
return true;
@@ -77,20 +80,20 @@ namespace CryptoExchange.Net.SharedApis
public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity)
{
var supportedType = GetSupportedQuantityType(side, type);
if (supportedType == SharedQuantityType.BaseAndQuoteAsset)
if (supportedType == SharedQuantityType.BaseAndQuoteAsset || supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
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;
}
@@ -1,14 +1,27 @@
namespace CryptoExchange.Net.SharedApis
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Symbol request
/// </summary>
public record SharedSymbolRequest : SharedRequest
{
/// <summary>
/// Trading mode
/// </summary>
public TradingMode TradingMode { get; }
/// <summary>
/// The symbol
/// </summary>
public SharedSymbol Symbol { get; set; }
public SharedSymbol? Symbol { get; set; }
/// <summary>
/// Symbols
/// </summary>
public SharedSymbol[]? Symbols { get; set; }
/// <summary>
/// ctor
@@ -16,6 +29,22 @@
public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
{
Symbol = symbol;
TradingMode = symbol.TradingMode;
}
/// <summary>
/// ctor
/// </summary>
public SharedSymbolRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
{
if (!symbols.Any())
throw new ArgumentException("Empty symbol list");
if (symbols.GroupBy(x => x.TradingMode).Count() > 1)
throw new ArgumentException("All symbols in the symbol list should have the same trading mode");
Symbols = symbols.ToArray();
TradingMode = Symbols.First().TradingMode;
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to book ticker updates
@@ -13,5 +15,22 @@
public SubscribeBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeBookTickerRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeBookTickerRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to kline/candlestick updates
@@ -20,5 +22,26 @@
{
Interval = interval;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="interval">Kline interval</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeKlineRequest(IEnumerable<SharedSymbol> symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
Interval = interval;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="interval">Kline interval</param>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeKlineRequest(SharedKlineInterval interval, params SharedSymbol[] symbols) : base(symbols, null)
{
Interval = interval;
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to order book snapshot updates
@@ -20,5 +22,22 @@
{
Limit = limit;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeOrderBookRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeOrderBookRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -1,4 +1,7 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to ticker updates
@@ -13,5 +16,22 @@
public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeTickerRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeTickerRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to trade updates
@@ -13,5 +15,22 @@
public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeTradeRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeTradeRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
}
}
@@ -30,5 +30,8 @@ namespace CryptoExchange.Net.SharedApis
public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType)
{
}
/// <inheritdoc />
public override SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant(), DeliveryTime) { SymbolName = Name };
}
}
@@ -7,6 +7,10 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public record SharedPosition : SharedSymbolModel
{
/// <summary>
/// Position id
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Current size of the position
/// </summary>
@@ -69,5 +69,11 @@
Name = symbol;
Trading = trading;
}
/// <summary>
/// The SharedSymbol of this symbol
/// </summary>
/// <returns></returns>
public virtual SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant()) { SymbolName = Name };
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
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>
+3 -5
View File
@@ -187,11 +187,8 @@ namespace CryptoExchange.Net.Sockets
return CallResult.SuccessResult;
CurrentResponses++;
if (CurrentResponses == RequiredResponses)
{
Completed = true;
if (CurrentResponses == RequiredResponses)
Response = message.Data;
}
if (Result?.Success != false)
// If an error result is already set don't override that
@@ -199,6 +196,7 @@ namespace CryptoExchange.Net.Sockets
if (CurrentResponses == RequiredResponses)
{
Completed = true;
_event.Set();
if (ContinueAwaiter != null)
await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
@@ -221,7 +219,7 @@ namespace CryptoExchange.Net.Sockets
return;
Completed = true;
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
Result = new CallResult<THandlerResponse>(new TimeoutError());
ContinueAwaiter?.Set();
_event.Set();
}
@@ -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<CallResult> 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());
}
/// <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)
{
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)
@@ -864,7 +864,7 @@ namespace CryptoExchange.Net.Sockets
{
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info));
}
@@ -899,7 +899,7 @@ namespace CryptoExchange.Net.Sockets
{
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info));
}
+15 -11
View File
@@ -131,17 +131,21 @@ namespace CryptoExchange.Net.Testing
var headers = new Dictionary<string, string>();
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
authProvider.AuthenticateRequest(
client,
new Uri(host.AppendPath(path)),
method,
ref uriParams,
ref bodyParams,
ref headers,
true,
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
authProvider.ProcessRequest(
client,
new RestRequestConfiguration(
new RequestDefinition(path, method)
{
Authenticated = true
},
host,
uriParams ?? new Dictionary<string, object>(),
bodyParams ?? new Dictionary<string, object>(),
headers,
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
)
);
var signature = getSignature(uriParams, bodyParams, headers);
@@ -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;
}
@@ -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;
}
+1 -1
View File
@@ -1,4 +1,4 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
+22 -21
View File
@@ -5,28 +5,29 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="11.1.0" />
<PackageReference Include="Bitfinex.Net" Version="9.1.0" />
<PackageReference Include="BitMart.Net" Version="2.1.0" />
<PackageReference Include="Bybit.Net" Version="5.1.0" />
<PackageReference Include="CoinEx.Net" Version="9.1.0" />
<PackageReference Include="CryptoCom.Net" Version="2.1.0" />
<PackageReference Include="DeepCoin.Net" Version="2.1.0" />
<PackageReference Include="GateIo.Net" Version="2.1.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.1.1" />
<PackageReference Include="JK.BingX.Net" Version="2.1.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" />
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.1.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" />
<PackageReference Include="Kucoin.Net" Version="7.1.0" />
<PackageReference Include="Binance.Net" Version="11.3.0" />
<PackageReference Include="Bitfinex.Net" Version="9.3.0" />
<PackageReference Include="BitMart.Net" Version="2.4.1" />
<PackageReference Include="Bybit.Net" Version="5.4.0" />
<PackageReference Include="CoinEx.Net" Version="9.3.0" />
<PackageReference Include="CoinW.Net" Version="1.0.1" />
<PackageReference Include="CryptoCom.Net" Version="2.4.0" />
<PackageReference Include="DeepCoin.Net" Version="2.3.0" />
<PackageReference Include="GateIo.Net" Version="2.4.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.4.0" />
<PackageReference Include="JK.BingX.Net" Version="2.3.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.3.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.3.0" />
<PackageReference Include="JK.OKX.Net" Version="3.3.1" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.3.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.3.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.3.1" />
<PackageReference Include="Kucoin.Net" Version="7.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Toobit.Net" Version="1.0.1" />
<PackageReference Include="WhiteBit.Net" Version="2.1.0" />
<PackageReference Include="XT.Net" Version="2.1.0" />
<PackageReference Include="Toobit.Net" Version="1.2.1" />
<PackageReference Include="WhiteBit.Net" Version="2.4.0" />
<PackageReference Include="XT.Net" Version="2.3.1" />
</ItemGroup>
</Project>
+5
View File
@@ -8,6 +8,7 @@
@inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient
@inject ICoinExRestClient coinexClient
@inject ICoinWRestClient coinWClient
@inject ICryptoComRestClient cryptocomClient
@inject IDeepCoinRestClient deepCoinClient
@inject IGateIoRestClient gateioClient
@@ -41,6 +42,7 @@
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync();
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
@@ -83,6 +85,9 @@
if (coinexTask.Result.Success)
_prices.Add("CoinEx", coinexTask.Result.Data.Single().LastPrice);
if (coinWTask.Result.Success)
_prices.Add("CoinW", coinWTask.Result.Data.Single(x => x.Symbol == "BTC_USDT").LastPrice);
if (cryptocomTask.Result.Success)
_prices.Add("CryptoCom", cryptocomTask.Result.Data.First().LastPrice ?? 0);
@@ -8,6 +8,7 @@
@inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient
@inject ICoinExSocketClient coinExSocketClient
@inject ICoinWSocketClient coinWSocketClient
@inject ICryptoComSocketClient cryptocomSocketClient
@inject IDeepCoinSocketClient deepCoinSocketClient
@inject IGateIoSocketClient gateioSocketClient
@@ -49,6 +50,7 @@
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)),
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
+4 -1
View File
@@ -9,6 +9,7 @@
@using BitMEX.Net.Interfaces;
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Authentication
@using CryptoExchange.Net.Interfaces
@@ -34,6 +35,7 @@
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject ICoinWOrderBookFactory coinWFactory
@inject ICryptoComOrderBookFactory cryptocomFactory
@inject IDeepCoinOrderBookFactory deepCoinFactory
@inject IGateIoOrderBookFactory gateioFactory
@@ -88,6 +90,7 @@
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
// DeepCoin does not support the ETH/BTC pair
@@ -115,7 +118,7 @@
public void Dispose()
{
_timer.Stop();
_timer?.Stop();
_timer.Dispose();
foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
// It's not necessary to wait for this
+5 -2
View File
@@ -9,6 +9,7 @@
@using BitMart.Net.Interfaces;
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Interfaces
@using CryptoCom.Net.Interfaces
@@ -35,6 +36,7 @@
@inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory
@inject ICoinExTrackerFactory coinExFactory
@inject ICoinWTrackerFactory coinWFactory
@inject ICryptoComTrackerFactory cryptocomFactory
@inject IDeepCoinTrackerFactory deepCoinFactory
@inject IGateIoTrackerFactory gateioFactory
@@ -82,6 +84,7 @@
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinWFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
@@ -121,8 +124,8 @@
public void Dispose()
{
_timer.Stop();
_timer.Dispose();
_timer?.Stop();
_timer?.Dispose();
foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected))
// It's not necessary to wait for this
_ = tracker.StopAsync();
+1
View File
@@ -41,6 +41,7 @@ namespace BlazorClient
services.AddBybit();
services.AddCoinbase();
services.AddCoinEx();
services.AddCoinW();
services.AddCryptoCom();
services.AddDeepCoin();
services.AddGateIo();
+1
View File
@@ -17,6 +17,7 @@
@using Bybit.Net.Interfaces.Clients;
@using Coinbase.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients;
@using CoinW.Net.Interfaces.Clients;
@using CryptoCom.Net.Interfaces.Clients;
@using DeepCoin.Net.Interfaces.Clients;
@using GateIo.Net.Interfaces.Clients;
+18
View File
@@ -21,6 +21,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|![CoinEx](https://raw.githubusercontent.com/JKorf/CoinEx.Net/refs/heads/master/CoinEx.Net/Icon/icon.png)|CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
|![CoinW](https://raw.githubusercontent.com/JKorf/CoinW.Net/refs/heads/main/CoinW.Net/Icon/icon.png)|CoinW|CEX|[JKorf/CoinW.Net](https://github.com/JKorf/CoinW.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinW.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinW.Net)|[Link](https://www.coinw.com/register?rc=rbtnp)|-|
|![CoinGecko](https://raw.githubusercontent.com/JKorf/CoinGecko.Net/refs/heads/main/CoinGecko.Net/Icon/icon.png)|CoinGecko|-|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|-|-|
|![Crypto.com](https://raw.githubusercontent.com/JKorf/CryptoCom.Net/refs/heads/main/CryptoCom.Net/Icon/icon.png)|Crypto.com|CEX|[JKorf/CryptoCom.Net](https://github.com/JKorf/CryptoCom.Net)|[![Nuget version](https://img.shields.io/nuget/v/CryptoCom.net.svg?style=flat-square)](https://www.nuget.org/packages/CryptoCom.Net)|[Link](https://crypto.com/exch/26ge92xbkn)|-|
|![DeepCoin](https://raw.githubusercontent.com/JKorf/DeepCoin.Net/refs/heads/main/DeepCoin.Net/Icon/icon.png)|DeepCoin|CEX|[JKorf/DeepCoin.Net](https://github.com/JKorf/DeepCoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/DeepCoin.net.svg?style=flat-square)](https://www.nuget.org/packages/DeepCoin.Net)|[Link](https://s.deepcoin.com/jddhfca)|-|
@@ -58,6 +59,23 @@ 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).
## 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
* Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported
* Version 9.3.1 - 29 Jul 2025
* Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum
* Added Id property to SharedPosition model
* Version 9.3.0 - 23 Jul 2025
* Updated websocket message to listener matching logic to be more flexible
* Updated decimal parser to support "NaN" and "-Infinity" strings, added check for negative overflow value, improved performance in most cases