mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a5832cccd | |||
| 4e47c4cbdf | |||
| 2af1520ecc | |||
| cf397af3ab | |||
| a1479705e2 | |||
| 175e23f110 | |||
| 9b7019ded2 | |||
| 7904aa9ba7 | |||
| 3fe6db589f | |||
| 625dccbbe4 | |||
| e650771d16 | |||
| 3dad28b19d | |||
| 2b9fda985e | |||
| ff8759409b | |||
| 0d9627c13f | |||
| 0179fd7e2a | |||
| b8d0b0cf95 | |||
| 73c42bd452 | |||
| 290be7f5e0 | |||
| 0be1bb16e3 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if !NETSTANDARD2_1
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
|
||||
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
@@ -403,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
return Convert.ToHexString(buff);
|
||||
#else
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -439,6 +443,16 @@ namespace CryptoExchange.Net.Authentication
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the serialized request body
|
||||
/// </summary>
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
|
||||
/// <returns>Cached value if it was in cache</returns>
|
||||
public object? Get(string key, TimeSpan maxAge)
|
||||
{
|
||||
_cache.TryGetValue(key, out CacheItem value);
|
||||
_cache.TryGetValue(key, out CacheItem? value);
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -93,6 +93,17 @@ namespace CryptoExchange.Net.Clients
|
||||
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>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Version of the CryptoExchange.Net base library
|
||||
/// </summary>
|
||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version;
|
||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the client implementation
|
||||
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Clients
|
||||
lock(_versionLock)
|
||||
{
|
||||
if (_exchangeVersion == null)
|
||||
_exchangeVersion = GetType().Assembly.GetName().Version;
|
||||
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
||||
|
||||
return _exchangeVersion;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="cancellationToken">Cancellation token</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="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
@@ -161,7 +162,8 @@ namespace CryptoExchange.Net.Clients
|
||||
ParameterCollection? parameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null) where T : class
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null)
|
||||
{
|
||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||
return SendAsync<T>(
|
||||
@@ -171,7 +173,8 @@ namespace CryptoExchange.Net.Clients
|
||||
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||
cancellationToken,
|
||||
additionalHeaders,
|
||||
weight);
|
||||
weight,
|
||||
weightSingleLimiter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -185,6 +188,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="cancellationToken">Cancellation token</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="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
@@ -193,7 +197,8 @@ namespace CryptoExchange.Net.Clients
|
||||
ParameterCollection? bodyParameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null) where T : class
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null)
|
||||
{
|
||||
string? cacheKey = null;
|
||||
if (ShouldCache(definition))
|
||||
@@ -217,7 +222,7 @@ namespace CryptoExchange.Net.Clients
|
||||
currentTry++;
|
||||
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)
|
||||
return new WebCallResult<T>(prepareResult.Error!);
|
||||
|
||||
@@ -258,6 +263,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="additionalHeaders">Additional headers 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>
|
||||
/// <exception cref="Exception"></exception>
|
||||
protected virtual async Task<CallResult> PrepareAsync(
|
||||
@@ -266,10 +272,9 @@ namespace CryptoExchange.Net.Clients
|
||||
RequestDefinition definition,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null)
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null)
|
||||
{
|
||||
var requestWeight = weight ?? definition.Weight;
|
||||
|
||||
// Time sync
|
||||
if (definition.Authenticated)
|
||||
{
|
||||
@@ -295,6 +300,7 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
var requestWeight = weight ?? definition.Weight;
|
||||
if (requestWeight != 0)
|
||||
{
|
||||
if (definition.RateLimitGate == null)
|
||||
@@ -316,7 +322,8 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
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)
|
||||
return new CallResult(limitResult.Error!);
|
||||
}
|
||||
@@ -617,7 +624,7 @@ namespace CryptoExchange.Net.Clients
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
|
||||
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)}]"));
|
||||
|
||||
TotalRequestsMade++;
|
||||
@@ -693,10 +700,21 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
// Json response received
|
||||
var parsedError = TryParseError(accessor);
|
||||
var parsedError = TryParseError(response.ResponseHeaders, accessor);
|
||||
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
|
||||
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>();
|
||||
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>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <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>
|
||||
/// 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
|
||||
return false;
|
||||
|
||||
if ((int?)callResult.ResponseStatusCode == 429
|
||||
if (callResult.Error is ServerRateLimitError
|
||||
&& ClientOptions.RateLimiterEnabled
|
||||
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
|
||||
&& gate != null)
|
||||
@@ -807,7 +826,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||
{
|
||||
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>();
|
||||
@@ -889,8 +908,8 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
// Write the parameters as json in the body
|
||||
string stringData;
|
||||
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
|
||||
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
stringData = CreateSerializer().Serialize(value);
|
||||
else
|
||||
stringData = CreateSerializer().Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
@@ -961,6 +980,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns>Server time</returns>
|
||||
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()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="queryDelegate"></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
|
||||
{
|
||||
@@ -422,9 +422,10 @@ namespace CryptoExchange.Net.Clients
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult(result.Error)!;
|
||||
}
|
||||
|
||||
_logger.Authenticated(socket.SocketId);
|
||||
}
|
||||
|
||||
_logger.Authenticated(socket.SocketId);
|
||||
socket.Authenticated = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
@@ -710,6 +711,25 @@ namespace CryptoExchange.Net.Clients
|
||||
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>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
|
||||
var result = Activator.CreateInstance(objectType);
|
||||
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)
|
||||
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
var 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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
|
||||
count++;
|
||||
}
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
continue;
|
||||
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
object? value;
|
||||
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)
|
||||
{
|
||||
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
}
|
||||
else if ((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();
|
||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
last = arrayProp.Index;
|
||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
||||
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))
|
||||
serializer.Serialize(writer, prop.GetValue(value));
|
||||
else
|
||||
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
}
|
||||
|
||||
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 =>
|
||||
(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
|
||||
{
|
||||
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = reader.Value!.ToString();
|
||||
var value = reader.Value!.ToString()!;
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (OverflowException)
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
/// </returns>
|
||||
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 (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
|
||||
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());
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
|
||||
using var jsonTextReader = new JsonTextReader(reader);
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class ArrayPropertyInfo
|
||||
@@ -79,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
JsonSerializerOptions? typeOptions = null;
|
||||
if (prop.JsonConverterType != null)
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType);
|
||||
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
|
||||
typeOptions = new JsonSerializerOptions();
|
||||
typeOptions.Converters.Clear();
|
||||
typeOptions.Converters.Add(converter);
|
||||
@@ -90,7 +90,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (prop.PropertyInfo.PropertyType == typeof(string))
|
||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
else
|
||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return default;
|
||||
|
||||
var result = Activator.CreateInstance(typeToConvert);
|
||||
var result = Activator.CreateInstance(typeToConvert)!;
|
||||
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))
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType);
|
||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
|
||||
newOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
||||
@@ -209,7 +209,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
|
||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : value);
|
||||
attribute.PropertyInfo.SetValue(result, value);
|
||||
else
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
try
|
||||
{
|
||||
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch(OverflowException)
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class BoolConverterInner<T> : JsonConverter<T>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
var dtValue = (DateTime)(object)value;
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
||||
{
|
||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters);
|
||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<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>8.4.5</PackageVersion>
|
||||
<AssemblyVersion>8.4.5</AssemblyVersion>
|
||||
<FileVersion>8.4.5</FileVersion>
|
||||
<PackageVersion>8.7.0</PackageVersion>
|
||||
<AssemblyVersion>8.7.0</AssemblyVersion>
|
||||
<FileVersion>8.7.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>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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++)
|
||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||
#else
|
||||
@@ -261,6 +261,7 @@ namespace CryptoExchange.Net
|
||||
if (price != null)
|
||||
{
|
||||
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
||||
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
|
||||
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
|
||||
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -111,7 +111,7 @@ namespace CryptoExchange.Net
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||
}
|
||||
}
|
||||
return formData.ToString();
|
||||
return formData.ToString()!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -366,7 +366,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
using var decompressedStream = new MemoryStream();
|
||||
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());
|
||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||
deflateStream.CopyTo(decompressedStream);
|
||||
@@ -435,6 +435,8 @@ namespace CryptoExchange.Net
|
||||
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
|
||||
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||
|
||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
@@ -31,5 +32,12 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="credentials"></param>
|
||||
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="httpClient">Optional shared http client instance</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>
|
||||
/// <returns></returns>
|
||||
Task CloseAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Update proxy setting
|
||||
/// </summary>
|
||||
void UpdateProxy(ApiProxy? proxy);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-2
@@ -35,6 +35,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
|
||||
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
|
||||
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
|
||||
|
||||
static CryptoExchangeWebSocketClientLoggingExtension()
|
||||
{
|
||||
@@ -169,7 +170,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
|
||||
|
||||
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
|
||||
LogLevel.Debug,
|
||||
LogLevel.Warning,
|
||||
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
|
||||
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
|
||||
|
||||
@@ -180,9 +181,14 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
|
||||
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1028, "SocketProcessingStateChanged"),
|
||||
new EventId(1029, "SocketProcessingStateChanged"),
|
||||
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
|
||||
|
||||
_socketPingTimeout = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
new EventId(1030, "SocketPingTimeout"),
|
||||
"[Sckt {Id}] ping frame timeout; reconnecting socket");
|
||||
|
||||
}
|
||||
|
||||
public static void SocketConnecting(
|
||||
@@ -358,5 +364,11 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
|
||||
}
|
||||
|
||||
public static void SocketPingTimeout(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_socketPingTimeout(logger, socketId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_sendingData = LoggerMessage.Define<int, int, string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(2028, "SendingData"),
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}");
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
|
||||
|
||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Warning,
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <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.
|
||||
if (x == null && y == null) return 0;
|
||||
|
||||
@@ -235,4 +235,18 @@
|
||||
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="y"></param>
|
||||
/// <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.
|
||||
if (x == null && y == null) return 0;
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddSecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddOptionalSecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -187,7 +187,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddEnumAsInt<T>(string key, T value)
|
||||
{
|
||||
var stringVal = EnumConverter.GetString(value);
|
||||
var stringVal = EnumConverter.GetString(value)!;
|
||||
Add(key, int.Parse(stringVal)!);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,38 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = 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)
|
||||
{
|
||||
@@ -73,7 +102,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ParameterPosition = parameterPosition,
|
||||
PreventCaching = preventCaching ?? false
|
||||
};
|
||||
_definitions.TryAdd(method + path, def);
|
||||
_definitions.TryAdd(identifier, def);
|
||||
}
|
||||
|
||||
return def;
|
||||
|
||||
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
|
||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,9 +843,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
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>
|
||||
/// Apply guard per connection
|
||||
/// </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>
|
||||
/// Apply guard per API key
|
||||
/// </summary>
|
||||
|
||||
@@ -68,8 +68,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="baseAddress">The host address</param>
|
||||
/// <param name="apiKey">The API key</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>
|
||||
/// <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,
|
||||
string host,
|
||||
string? apiKey,
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour rateLimitingBehaviour,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
_waitingCount++;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.Requests
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => _request.RequestUri;
|
||||
public Uri Uri => _request.RequestUri!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int RequestId { get; }
|
||||
|
||||
@@ -17,28 +17,7 @@ namespace CryptoExchange.Net.Requests
|
||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
client = CreateClient(proxy, requestTimeout);
|
||||
|
||||
_httpClient = client;
|
||||
}
|
||||
@@ -51,5 +30,37 @@ namespace CryptoExchange.Net.Requests
|
||||
|
||||
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
|
||||
{
|
||||
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}");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,13 +113,13 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
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}");
|
||||
}
|
||||
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}");
|
||||
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}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override string ToString(string 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
if (Name != null)
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
/// </summary>
|
||||
public int? PriceDecimals { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of significant figures to use for price. For example with value of 5 these values are valid: 0.00001, 0.12300, 123.53, 12345, but this is not: 12345.1
|
||||
/// </summary>
|
||||
public int? PriceSignificantFigures { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the symbol is currently available for trading
|
||||
/// </summary>
|
||||
public bool Trading { get; set; }
|
||||
|
||||
@@ -155,6 +155,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
_baseAddress = $"{Uri.Scheme}://{Uri.Host}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProxy(ApiProxy? proxy)
|
||||
{
|
||||
Parameters.Proxy = proxy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<CallResult> ConnectAsync()
|
||||
{
|
||||
@@ -189,9 +195,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
|
||||
if (Parameters.Proxy != null)
|
||||
SetProxy(socket, Parameters.Proxy);
|
||||
#if NET6_0_OR_GREATER
|
||||
#if NET6_0_OR_GREATER
|
||||
socket.Options.CollectHttpResponseDetails = true;
|
||||
#endif
|
||||
#endif
|
||||
#if NET9_0_OR_GREATER
|
||||
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
|
||||
#endif
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
@@ -229,13 +238,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (e is WebSocketException we)
|
||||
{
|
||||
#if (NET6_0_OR_GREATER)
|
||||
#if (NET6_0_OR_GREATER)
|
||||
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
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
|
||||
// Try to read 429 from the message instead
|
||||
if (we.Message.Contains("429"))
|
||||
@@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
return new CallResult(new ServerRateLimitError(we.Message));
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
return new CallResult(new CantConnectError());
|
||||
@@ -435,8 +444,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Wait until we receive close confirmation
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5))
|
||||
break; // Wait for max 5 seconds, then just abort the connection
|
||||
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
|
||||
break; // Wait for max 1 second, then just abort the connection
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,15 +587,27 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_receivedMessagesLock)
|
||||
_receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
if (ex.InnerException?.InnerException?.Message.Equals("The WebSocket didn't recieve a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.") == true)
|
||||
{
|
||||
// Spefic case that the websocket connection got closed because of a ping frame timeout
|
||||
// Unfortunately doesn't seem to be a nicer way to catch
|
||||
_logger.SocketPingTimeout(Id);
|
||||
}
|
||||
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
|
||||
// canceled
|
||||
break;
|
||||
}
|
||||
catch (Exception wse)
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
if (!_ctsSource.Token.IsCancellationRequested)
|
||||
// Connection closed unexpectedly
|
||||
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
break;
|
||||
@@ -598,14 +619,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_socket.State == WebSocketState.CloseReceived)
|
||||
{
|
||||
// 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)
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
@@ -620,7 +641,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Write the data to a memory stream to be reassembled later
|
||||
if (multipartStream == null)
|
||||
multipartStream = new MemoryStream();
|
||||
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count);
|
||||
multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -634,7 +655,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Received the end of a multipart message, write to memory stream for reassembling
|
||||
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
|
||||
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count);
|
||||
multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Callback after query
|
||||
/// </summary>
|
||||
public Action<CallResult>? Callback { get; set; }
|
||||
public Action<SocketConnection, CallResult>? Callback { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public bool Completed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for the request
|
||||
/// </summary>
|
||||
public TimeSpan? RequestTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
||||
@@ -11,6 +11,8 @@ using System.Diagnostics;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
@@ -396,7 +398,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
|
||||
{
|
||||
Query query;
|
||||
Query? query;
|
||||
lock (_listenersLock)
|
||||
{
|
||||
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>
|
||||
protected virtual Task HandleRequestSentAsync(int requestId)
|
||||
{
|
||||
Query query;
|
||||
Query? query;
|
||||
lock (_listenersLock)
|
||||
{
|
||||
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||
@@ -437,7 +439,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
query.IsSend(ApiClient.ClientOptions.RequestTimeout);
|
||||
query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -583,6 +585,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
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>
|
||||
/// Close the connection
|
||||
/// </summary>
|
||||
@@ -988,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="queryDelegate">Method returning the query to send</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)
|
||||
throw new ArgumentNullException(nameof(queryDelegate));
|
||||
@@ -1020,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
try
|
||||
{
|
||||
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
|
||||
callback?.Invoke(result);
|
||||
callback?.Invoke(this, result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -378,7 +378,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -5,8 +5,11 @@ namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
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"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
@@ -14,8 +17,11 @@ namespace CryptoExchange.Net.Testing
|
||||
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"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
|
||||
@@ -25,5 +25,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
_request.RequestId = requestId;
|
||||
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 void Dispose() { }
|
||||
|
||||
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +150,9 @@ namespace CryptoExchange.Net.Testing
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient"></typeparam>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static void CheckForMissingRestInterfaces<TClient>()
|
||||
public static void CheckForMissingRestInterfaces<TClient>(string[]? excludeInterfaces = null)
|
||||
{
|
||||
CheckForMissingInterfaces(typeof(TClient), typeof(Task));
|
||||
CheckForMissingInterfaces(typeof(TClient), typeof(Task), excludeInterfaces);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,28 +160,32 @@ namespace CryptoExchange.Net.Testing
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient"></typeparam>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static void CheckForMissingSocketInterfaces<TClient>()
|
||||
public static void CheckForMissingSocketInterfaces<TClient>(string[]? excludeInterfaces = null)
|
||||
{
|
||||
CheckForMissingInterfaces(typeof(TClient), typeof(Task<CallResult<UpdateSubscription>>));
|
||||
CheckForMissingInterfaces(typeof(TClient), typeof(Task<CallResult<UpdateSubscription>>), excludeInterfaces);
|
||||
}
|
||||
|
||||
private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes)
|
||||
private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes, string[]? excludeInterfaces = null)
|
||||
{
|
||||
var assembly = Assembly.GetAssembly(clientType);
|
||||
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")
|
||||
&& (excludeInterfaces?.Contains(t.Name) != true));
|
||||
|
||||
foreach (var clientInterface in clientInterfaces)
|
||||
{
|
||||
var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||
var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && !t.IsInterface && t != clientInterface);
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
int methods = 0;
|
||||
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());
|
||||
if (interfaceMethod == null)
|
||||
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||
var interfaceMethod =
|
||||
clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray())
|
||||
?? clientInterface.GetInterfaces().Select(x => x.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray())).FirstOrDefault()
|
||||
?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||
methods++;
|
||||
}
|
||||
|
||||
|
||||
@@ -350,7 +350,8 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
_data.Add(item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(v => v.Timestamp);
|
||||
if (_data.Any())
|
||||
_firstTimestamp = _data.Min(v => v.Timestamp);
|
||||
|
||||
ApplyWindow(false);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,33 @@ 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 8.7.0 - 21 Jan 2025
|
||||
* Added GetMillisecondTimestampLong helper method to AuthenticationProvider
|
||||
* Added PriceSignificationFigures to SharedSpotSymbol model
|
||||
|
||||
* Version 8.6.1 - 09 Jan 2025
|
||||
* Fixed websocket connection getting stuck after a ping frame timeout
|
||||
* Removed websocket Error callback when exception is expected
|
||||
* Removed unnecessary type restraints on RestApiClient.SendAsync methods
|
||||
|
||||
* 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
|
||||
* Added EmptyArrayObjectConverter System.Text.Json JsonConverter
|
||||
* Added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor constructor
|
||||
|
||||
Reference in New Issue
Block a user