1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Jkorf 3fe6db589f Updated to version 8.6.0 2025-01-07 13:25:50 +01:00
Jkorf 625dccbbe4 Added ExchangeType enum, some small improvements 2025-01-07 13:22:16 +01:00
Jkorf e650771d16 Added response headers parameter to RestApiClient.TryParseError method, added check for ServerRateLimitError on the result 2025-01-07 10:19:37 +01:00
Jkorf 3dad28b19d Added IFeeRestClient to service registration 2025-01-07 08:59:51 +01:00
Jkorf 2b9fda985e Add support for passing weight to apply to an individual ratelimit guard 2025-01-07 08:35:06 +01:00
JKorf ff8759409b Use Convert.ToHexString if available 2025-01-06 21:38:42 +01:00
JKorf 0d9627c13f Changed socket no data reconnect message to LogLevel Warning 2024-12-23 20:03:56 +01:00
Jkorf 0179fd7e2a Fixed workflow automated tests 2024-12-23 14:43:23 +01:00
Jkorf b8d0b0cf95 Workflow fix 2024-12-23 14:36:18 +01:00
Jkorf 73c42bd452 Updated to version 8.5.0 2024-12-23 14:25:39 +01:00
Jkorf 290be7f5e0 Added net9.0 build target, added KeepAliveTimeout for websocket connections 2024-12-23 14:14:47 +01:00
Jan Korf 0be1bb16e3 Feature/update settings (#225)
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
2024-12-23 08:49:58 +01:00
53 changed files with 354 additions and 130 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
@@ -1,4 +1,4 @@
#if !NETSTANDARD2_1 #if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis namespace System.Diagnostics.CodeAnalysis
{ {
using System; using System;
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create(); var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem) if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{ {
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key // Read from pem private key
var key = _credentials.Secret! var key = _credentials.Secret!
.Replace("\n", "") .Replace("\n", "")
@@ -403,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static string BytesToHexString(byte[] buff) protected static string BytesToHexString(byte[] buff)
{ {
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty; var result = string.Empty;
foreach (var t in buff) foreach (var t in buff)
result += t.ToString("X2"); result += t.ToString("X2");
return result; return result;
#endif
} }
/// <summary> /// <summary>
+1 -1
View File
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns> /// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge) public object? Get(string key, TimeSpan maxAge)
{ {
_cache.TryGetValue(key, out CacheItem value); _cache.TryGetValue(key, out CacheItem? value);
if (value == null) if (value == null)
return null; return null;
@@ -93,6 +93,17 @@ namespace CryptoExchange.Net.Clients
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
} }
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+2 -2
View File
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Version of the CryptoExchange.Net base library /// Version of the CryptoExchange.Net base library
/// </summary> /// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version; public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary> /// <summary>
/// Version of the client implementation /// Version of the client implementation
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Clients
lock(_versionLock) lock(_versionLock)
{ {
if (_exchangeVersion == null) if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version; _exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion; return _exchangeVersion;
} }
+43 -16
View File
@@ -154,6 +154,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>( protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -161,7 +162,8 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? parameters, ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null,
int? weightSingleLimiter = null) where T : class
{ {
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>( return SendAsync<T>(
@@ -171,7 +173,8 @@ namespace CryptoExchange.Net.Clients
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken, cancellationToken,
additionalHeaders, additionalHeaders,
weight); weight,
weightSingleLimiter);
} }
/// <summary> /// <summary>
@@ -185,6 +188,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>( protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -193,7 +197,8 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null,
int? weightSingleLimiter = null) where T : class
{ {
string? cacheKey = null; string? cacheKey = null;
if (ShouldCache(definition)) if (ShouldCache(definition))
@@ -217,7 +222,7 @@ namespace CryptoExchange.Net.Clients
currentTry++; currentTry++;
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter).ConfigureAwait(false);
if (!prepareResult) if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!); return new WebCallResult<T>(prepareResult.Error!);
@@ -258,6 +263,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param> /// <param name="weight">Override the request weight for this request</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync( protected virtual async Task<CallResult> PrepareAsync(
@@ -266,10 +272,9 @@ namespace CryptoExchange.Net.Clients
RequestDefinition definition, RequestDefinition definition,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) int? weight = null,
int? weightSingleLimiter = null)
{ {
var requestWeight = weight ?? definition.Weight;
// Time sync // Time sync
if (definition.Authenticated) if (definition.Authenticated)
{ {
@@ -295,6 +300,7 @@ namespace CryptoExchange.Net.Clients
} }
// Rate limiting // Rate limiting
var requestWeight = weight ?? definition.Weight;
if (requestWeight != 0) if (requestWeight != 0)
{ {
if (definition.RateLimitGate == null) if (definition.RateLimitGate == null)
@@ -316,7 +322,8 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult(limitResult.Error!); return new CallResult(limitResult.Error!);
} }
@@ -617,7 +624,7 @@ namespace CryptoExchange.Net.Clients
paramString = $" with request body '{request.Content}'"; paramString = $" with request body '{request.Content}'";
var headers = request.GetHeaders(); var headers = request.GetHeaders();
if (headers.Any()) if (headers.Count != 0)
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")); paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++; TotalRequestsMade++;
@@ -693,10 +700,21 @@ namespace CryptoExchange.Net.Clients
} }
// Json response received // Json response received
var parsedError = TryParseError(accessor); var parsedError = TryParseError(response.ResponseHeaders, accessor);
if (parsedError != null) if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
{
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
}
// Success status code, but TryParseError determined it was an error response // Success status code, but TryParseError determined it was an error response
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, parsedError); 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, parsedError);
}
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
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, deserializeResult.Data, deserializeResult.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, deserializeResult.Data, deserializeResult.Error);
@@ -730,12 +748,13 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not. /// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null /// If the response is an error this method should return the parsed error, else it should return null
/// </summary> /// </summary>
/// <param name="accessor">Data accessor</param> /// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns> /// <returns>Null if not an error, Error otherwise</returns>
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null; protected virtual Error? TryParseError(IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor) => null;
/// <summary> /// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@@ -752,7 +771,7 @@ namespace CryptoExchange.Net.Clients
// Only retry once // Only retry once
return false; return false;
if ((int?)callResult.ResponseStatusCode == 429 if (callResult.Error is ServerRateLimitError
&& ClientOptions.RateLimiterEnabled && ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null) && gate != null)
@@ -807,7 +826,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InUri) if (parameterPosition == HttpMethodParameterPosition.InUri)
{ {
foreach (var parameter in parameters) foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()); uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()!);
} }
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
@@ -889,8 +908,8 @@ namespace CryptoExchange.Net.Clients
{ {
// Write the parameters as json in the body // Write the parameters as json in the body
string stringData; string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey)) if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]); stringData = CreateSerializer().Serialize(value);
else else
stringData = CreateSerializer().Serialize(parameters); stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType); request.SetContent(stringData, contentType);
@@ -961,6 +980,14 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns> /// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
+22 -2
View File
@@ -158,7 +158,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -422,9 +422,10 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message; result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!; return new CallResult(result.Error)!;
} }
_logger.Authenticated(socket.SocketId);
} }
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true; socket.Authenticated = true;
return new CallResult(null); return new CallResult(null);
} }
@@ -710,6 +711,25 @@ namespace CryptoExchange.Net.Clients
return new CallResult(null); return new CallResult(null);
} }
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| !socketConnections.Any())
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
var result = Activator.CreateInstance(objectType); var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader); var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType); return ParseObject(arr, result!, objectType);
} }
private static object ParseObject(JArray arr, object result, Type objectType) private static object ParseObject(JArray arr, object result, Type objectType)
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
var count = 0; var count = 0;
if (innerArray.Count == 0) if (innerArray.Count == 0)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else if (innerArray[0].Type == JTokenType.Array) else if (innerArray[0].Type == JTokenType.Array)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
foreach (var obj in innerArray) foreach (var obj in innerArray)
{ {
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!); arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
count++; count++;
} }
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else else
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!); arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
continue; continue;
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
object? value; object? value;
if (converterAttribute != null) if (converterAttribute != null)
{ {
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
} }
else if (conversionAttribute != null) else if (conversionAttribute != null)
{ {
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
else if ((property.PropertyType == typeof(decimal) else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?)) || property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0)) && (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
var v = value.ToString(); var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec)) if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
last = arrayProp.Index; last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop); var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null) if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType))); writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
else if (!IsSimple(prop.PropertyType)) else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value)); serializer.Serialize(writer, prop.GetValue(value));
else else
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute => private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))); (T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
private static T? GetCustomAttribute<T>(Type type) where T : Attribute => private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))); (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
} }
} }
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
{ {
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
var value = reader.Value!.ToString(); var value = reader.Value!.ToString()!;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
/// </returns> /// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var value = reader.Value?.ToString().ToLower().Trim(); var value = reader.Value?.ToString()!.ToLower().Trim();
if (value == null || value == "") if (value == null || value == "")
{ {
if (Nullable.GetUnderlyingType(objectType) != null) if (Nullable.GetUnderlyingType(objectType) != null)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader); using var jsonTextReader = new JsonTextReader(reader);
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class ArrayPropertyInfo private class ArrayPropertyInfo
@@ -79,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonSerializerOptions? typeOptions = null; JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null) if (prop.JsonConverterType != null)
{ {
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
typeOptions = new JsonSerializerOptions(); typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear(); typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter); typeOptions.Converters.Add(converter);
@@ -90,7 +90,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (prop.PropertyInfo.PropertyType == typeof(string)) if (prop.PropertyInfo.PropertyType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
} }
else else
{ {
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
return default; return default;
var result = Activator.CreateInstance(typeToConvert); var result = Activator.CreateInstance(typeToConvert)!;
return (T)ParseObject(ref reader, result, typeToConvert, options); return (T)ParseObject(ref reader, result, typeToConvert, options);
} }
@@ -177,7 +177,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
{ {
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
newOptions = new JsonSerializerOptions newOptions = new JsonSerializerOptions
{ {
NumberHandling = SerializerOptions.WithConverters.NumberHandling, NumberHandling = SerializerOptions.WithConverters.NumberHandling,
@@ -209,7 +209,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value == null ? null : value); attribute.PropertyInfo.SetValue(result, value);
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch(OverflowException) catch(OverflowException)
{ {
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class BoolConverterInner<T> : JsonConverter<T> private class BoolConverterInner<T> : JsonConverter<T>
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class DateTimeConverterInner<T> : JsonConverter<T> private class DateTimeConverterInner<T> : JsonConverter<T>
@@ -74,7 +74,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
{
writer.WriteNullValue(); writer.WriteNullValue();
}
else else
{ {
var dtValue = (DateTime)(object)value; var dtValue = (DateTime)(object)value;
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert) public override JsonConverter CreateConverter(Type typeToConvert)
{ {
return (JsonConverter)Activator.CreateInstance(_type, _parameters); return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
} }
} }
+4 -4
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<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>8.4.5</PackageVersion> <PackageVersion>8.6.0</PackageVersion>
<AssemblyVersion>8.4.5</AssemblyVersion> <AssemblyVersion>8.6.0</AssemblyVersion>
<FileVersion>8.4.5</FileVersion> <FileVersion>8.6.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</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</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
+1 -1
View File
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net
{ {
var randomChars = new char[length]; var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else #else
+5 -3
View File
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&"; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
} }
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
@@ -111,7 +111,7 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
} }
} }
return formData.ToString(); return formData.ToString()!;
} }
/// <summary> /// <summary>
@@ -366,7 +366,7 @@ namespace CryptoExchange.Net
{ {
using var decompressedStream = new MemoryStream(); using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream); deflateStream.CopyTo(decompressedStream);
@@ -435,6 +435,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IWithdrawalRestClient)client(x)!); services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T))) if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawRestClient)client(x)!); services.AddTransient(x => (IWithdrawRestClient)client(x)!);
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!); services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
@@ -31,5 +32,12 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="credentials"></param> /// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials; void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api crentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
} }
} }
@@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param> /// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param> /// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null); void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
} }
} }
@@ -93,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task CloseAsync(); Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
} }
} }
@@ -169,7 +169,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}"); "[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>( _noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Debug, LogLevel.Warning,
new EventId(1027, "NoDataReceiveTimeoutReconnect"), new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket"); "[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(byte[] x, byte[] y) public int Compare(byte[]? x, byte[]? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
+14
View File
@@ -235,4 +235,18 @@
Cache Cache
} }
/// <summary>
/// Type of exchange
/// </summary>
public enum ExchangeType
{
/// <summary>
/// Centralized
/// </summary>
CEX,
/// <summary>
/// Decentralized
/// </summary>
DEX
}
} }
@@ -0,0 +1,29 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<T> where T : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public T? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}
/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
}
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(string x, string y) public int Compare(string? x, string? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
@@ -156,7 +156,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddSecondsString(string key, DateTime value) public void AddSecondsString(string key, DateTime value)
{ {
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.Objects
public void AddOptionalSecondsString(string key, DateTime? value) public void AddOptionalSecondsString(string key, DateTime? value)
{ {
if (value != null) if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -187,7 +187,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value) public void AddEnumAsInt<T>(string key, T value)
{ {
var stringVal = EnumConverter.GetString(value); var stringVal = EnumConverter.GetString(value)!;
Add(key, int.Parse(stringVal)!); Add(key, int.Parse(stringVal)!);
} }
@@ -58,9 +58,38 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{ {
if (!_definitions.TryGetValue(method + path, out var def)) if (!_definitions.TryGetValue(identifier, out var def))
{ {
def = new RequestDefinition(path, method) def = new RequestDefinition(path, method)
{ {
@@ -73,7 +102,7 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false PreventCaching = preventCaching ?? false
}; };
_definitions.TryAdd(method + path, def); _definitions.TryAdd(identifier, def);
} }
return def; return def;
+2 -2
View File
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow; TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{ {
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms"); Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
TimeSyncState.TimeOffset = TimeSpan.Zero; TimeSyncState.TimeOffset = TimeSpan.Zero;
} }
else else
{ {
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms"); Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
TimeSyncState.TimeOffset = offset; TimeSyncState.TimeOffset = offset;
} }
} }
@@ -843,9 +843,9 @@ namespace CryptoExchange.Net.OrderBook
internal class DescComparer<T> : IComparer<T> internal class DescComparer<T> : IComparer<T>
{ {
public int Compare(T x, T y) public int Compare(T? x, T? y)
{ {
return Comparer<T>.Default.Compare(y, x); return Comparer<T>.Default.Compare(y!, x!);
} }
} }
} }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Apply guard per connection /// Apply guard per connection
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()); public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
/// <summary> /// <summary>
/// Apply guard per API key /// Apply guard per API key
/// </summary> /// </summary>
@@ -68,8 +68,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="baseAddress">The host address</param> /// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct); Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
} }
} }
@@ -69,6 +69,7 @@ namespace CryptoExchange.Net.RateLimiting
RequestDefinition definition, RequestDefinition definition,
string host, string host,
string? apiKey, string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour, RateLimitingBehaviour rateLimitingBehaviour,
CancellationToken ct) CancellationToken ct)
{ {
@@ -77,7 +78,7 @@ namespace CryptoExchange.Net.RateLimiting
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
+1 -1
View File
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.Requests
} }
/// <inheritdoc /> /// <inheritdoc />
public Uri Uri => _request.RequestUri; public Uri Uri => _request.RequestUri!;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
+33 -22
View File
@@ -17,28 +17,7 @@ namespace CryptoExchange.Net.Requests
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
if (client == null) if (client == null)
{ client = CreateClient(proxy, requestTimeout);
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
_httpClient = client; _httpClient = client;
} }
@@ -51,5 +30,37 @@ namespace CryptoExchange.Net.Requests
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
} }
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
{
_httpClient = CreateClient(proxy, requestTimeout);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var client = new HttpClient(handler)
{
Timeout = requestTimeout
};
return client;
}
} }
} }
@@ -60,8 +60,8 @@ namespace CryptoExchange.Net.SharedApis
} }
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 new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -113,13 +113,13 @@ 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 new ArgumentError($"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 new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
public override string ToString(string exchange) public override string ToString(string exchange)
{ {
var sb = new StringBuilder(base.ToString(exchange)); var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Supported limit values: [{(SupportedLimits == null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (Name != null) if (Name != null)
return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}";
return $"[{ValueType.Name}] {string.Join(" / ", Names)}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}";
} }
} }
} }
@@ -155,6 +155,12 @@ namespace CryptoExchange.Net.Sockets
_baseAddress = $"{Uri.Scheme}://{Uri.Host}"; _baseAddress = $"{Uri.Scheme}://{Uri.Host}";
} }
/// <inheritdoc />
public void UpdateProxy(ApiProxy? proxy)
{
Parameters.Proxy = proxy;
}
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<CallResult> ConnectAsync() public virtual async Task<CallResult> ConnectAsync()
{ {
@@ -189,9 +195,12 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null) if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy); SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true; socket.Options.CollectHttpResponseDetails = true;
#endif #endif
#if NET9_0_OR_GREATER
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
#endif
} }
catch (PlatformNotSupportedException) catch (PlatformNotSupportedException)
{ {
@@ -229,13 +238,13 @@ namespace CryptoExchange.Net.Sockets
if (e is WebSocketException we) if (e is WebSocketException we)
{ {
#if (NET6_0_OR_GREATER) #if (NET6_0_OR_GREATER)
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
{ {
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#else #else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead // Try to read 429 from the message instead
if (we.Message.Contains("429")) if (we.Message.Contains("429"))
@@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#endif #endif
} }
return new CallResult(new CantConnectError()); return new CallResult(new CantConnectError());
@@ -435,8 +444,8 @@ namespace CryptoExchange.Net.Sockets
{ {
// Wait until we receive close confirmation // Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false); await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5)) if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
break; // Wait for max 5 seconds, then just abort the connection break; // Wait for max 1 second, then just abort the connection
} }
} }
} }
@@ -598,14 +607,14 @@ namespace CryptoExchange.Net.Sockets
if (_socket.State == WebSocketState.CloseReceived) if (_socket.State == WebSocketState.CloseReceived)
{ {
// Close received means it server initiated, we should send a confirmation and close the socket // Close received means it server initiated, we should send a confirmation and close the socket
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
if (_closeTask?.IsCompleted != false) if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
} }
else else
{ {
// Means the socket is now closed and we were the one initiating it // Means the socket is now closed and we were the one initiating it
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
} }
break; break;
@@ -620,7 +629,7 @@ namespace CryptoExchange.Net.Sockets
// Write the data to a memory stream to be reassembled later // Write the data to a memory stream to be reassembled later
if (multipartStream == null) if (multipartStream == null)
multipartStream = new MemoryStream(); multipartStream = new MemoryStream();
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
else else
{ {
@@ -634,7 +643,7 @@ namespace CryptoExchange.Net.Sockets
{ {
// Received the end of a multipart message, write to memory stream for reassembling // Received the end of a multipart message, write to memory stream for reassembling
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count); _logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
break; break;
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Callback after query /// Callback after query
/// </summary> /// </summary>
public Action<CallResult>? Callback { get; set; } public Action<SocketConnection, CallResult>? Callback { get; set; }
} }
} }
+5
View File
@@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool Completed { get; set; } public bool Completed { get; set; }
/// <summary>
/// Timeout for the request
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
/// <summary> /// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response /// and each symbol receives it's own confirmation response
+17 -5
View File
@@ -11,6 +11,8 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using System.Threading; using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -396,7 +398,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId) protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -425,7 +427,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="requestId">Id of the request sent</param> /// <param name="requestId">Id of the request sent</param>
protected virtual Task HandleRequestSentAsync(int requestId) protected virtual Task HandleRequestSentAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -437,7 +439,7 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask; return Task.CompletedTask;
} }
query.IsSend(ApiClient.ClientOptions.RequestTimeout); query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -583,6 +585,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
/// <summary>
/// Update the proxy setting and reconnect
/// </summary>
/// <param name="proxy">New proxy setting</param>
public async Task UpdateProxy(ApiProxy? proxy)
{
_socket.UpdateProxy(proxy);
await TriggerReconnectAsync().ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Close the connection /// Close the connection
/// </summary> /// </summary>
@@ -988,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="interval">How often</param> /// <param name="interval">How often</param>
/// <param name="queryDelegate">Method returning the query to send</param> /// <param name="queryDelegate">Method returning the query to send</param>
/// <param name="callback">The callback for processing the response</param> /// <param name="callback">The callback for processing the response</param>
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
if (queryDelegate == null) if (queryDelegate == null)
throw new ArgumentNullException(nameof(queryDelegate)); throw new ArgumentNullException(nameof(queryDelegate));
@@ -1020,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
callback?.Invoke(result); callback?.Invoke(this, result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -378,7 +378,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal) if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString())) if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
} }
} }
@@ -211,7 +211,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object) if (dictProp.Value.Type == JTokenType.Object)
{ {
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties); CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties);
} }
else else
{ {
@@ -5,8 +5,11 @@ namespace CryptoExchange.Net.Testing
{ {
internal class EnumValueTraceListener : TraceListener internal class EnumValueTraceListener : TraceListener
{ {
public override void Write(string message) public override void Write(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -14,8 +17,11 @@ namespace CryptoExchange.Net.Testing
throw new Exception("Enum null error: " + message); throw new Exception("Enum null error: " + message);
} }
public override void WriteLine(string message) public override void WriteLine(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -25,5 +25,7 @@ namespace CryptoExchange.Net.Testing.Implementations
_request.RequestId = requestId; _request.RequestId = requestId;
return _request; return _request;
} }
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
} }
} }
@@ -92,5 +92,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public Task ReconnectAsync() => throw new NotImplementedException(); public Task ReconnectAsync() => throw new NotImplementedException();
public void Dispose() { } public void Dispose() { }
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
} }
} }
+2 -4
View File
@@ -169,7 +169,7 @@ namespace CryptoExchange.Net.Testing
{ {
var assembly = Assembly.GetAssembly(clientType); var assembly = Assembly.GetAssembly(clientType);
var interfaceType = clientType.GetInterface("I" + clientType.Name); var interfaceType = clientType.GetInterface("I" + clientType.Name);
var clientInterfaces = assembly.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared")); var clientInterfaces = assembly!.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared"));
foreach (var clientInterface in clientInterfaces) foreach (var clientInterface in clientInterfaces)
{ {
@@ -179,9 +179,7 @@ namespace CryptoExchange.Net.Testing
int methods = 0; int methods = 0;
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType))) foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
{ {
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()); var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) ?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
if (interfaceMethod == null)
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
methods++; methods++;
} }
@@ -350,7 +350,8 @@ namespace CryptoExchange.Net.Trackers.Trades
_data.Add(item); _data.Add(item);
} }
_firstTimestamp = _data.Min(v => v.Timestamp); if (_data.Any())
_firstTimestamp = _data.Min(v => v.Timestamp);
ApplyWindow(false); ApplyWindow(false);
} }
+18
View File
@@ -66,6 +66,24 @@ 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 8.6.0 - 07 Jan 2025
* Added support for passing weight to apply to an individual ratelimit guard
* Added IFeeRestClient to service registration
* Added response headers parameter to RestApiClient.TryParseError method
* Added check for ServerRateLimitError on RestApiClient.TryParseError response
* Added ExchangeType Enum
* Some small improvements
* Version 8.5.0 - 23 Dec 2024
* Added SetOptions method to update client settings
* Added SocketConnection parameter to PeriodicQuery callback
* Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
* Added support for overriding request timeout per request
* Added build target for net9.0
* Added setting of KeepAliveTimeout on websocket connections to improve dropped connection detection
* Changed max wait time for close handshake response from 5 seconds to 1 second
* Fixed exception in trade tracker when there is no data in the initial snapshot
* Version 8.4.5 - 20 Dec 2024 * Version 8.4.5 - 20 Dec 2024
* Added EmptyArrayObjectConverter System.Text.Json JsonConverter * Added EmptyArrayObjectConverter System.Text.Json JsonConverter
* Added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor constructor * Added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor constructor