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

Compare commits

...

17 Commits

Author SHA1 Message Date
Jkorf 4a5832cccd Updated to version 8.7.0 2025-01-21 14:02:42 +01:00
Jkorf 4e47c4cbdf Updated CheckForMissingInterfaces test 2025-01-21 14:00:30 +01:00
Jkorf 2af1520ecc Added PriceSignificationFigures to SharedSpotSymbol model 2025-01-21 14:00:08 +01:00
Jkorf cf397af3ab Added GetMillisecondTimestampLong helper method to AuthenticationProvider 2025-01-21 13:47:05 +01:00
JKorf a1479705e2 Fixed typo 2025-01-13 17:49:38 +01:00
Jkorf 175e23f110 Updated to version 8.6.1 2025-01-09 16:25:11 +01:00
Jkorf 9b7019ded2 Removed websocket Error callback when exception is expected 2025-01-09 16:23:34 +01:00
Jkorf 7904aa9ba7 Fixed websocket connection getting stuck after a ping frame timeout, removed unnecessary type restraints on RestApiClient.SendAsync methods 2025-01-09 16:18:13 +01:00
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
19 changed files with 173 additions and 41 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>
@@ -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>
@@ -439,6 +443,16 @@ namespace CryptoExchange.Net.Authentication
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); 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> /// <summary>
/// Return the serialized request body /// Return the serialized request body
/// </summary> /// </summary>
+34 -15
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)
{ {
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)
{ {
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)
@@ -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);
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>8.5.0</PackageVersion> <PackageVersion>8.7.0</PackageVersion>
<AssemblyVersion>8.5.0</AssemblyVersion> <AssemblyVersion>8.7.0</AssemblyVersion>
<FileVersion>8.5.0</FileVersion> <FileVersion>8.7.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
View File
@@ -261,6 +261,7 @@ namespace CryptoExchange.Net
if (price != null) if (price != null)
{ {
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value); 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; adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue) if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{ {
+2
View File
@@ -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)!);
@@ -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?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect; 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, string, string, Exception?> _socketProcessingStateChanged;
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
static CryptoExchangeWebSocketClientLoggingExtension() static CryptoExchangeWebSocketClientLoggingExtension()
{ {
@@ -169,7 +170,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");
@@ -180,9 +181,14 @@ namespace CryptoExchange.Net.Logging.Extensions
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>( _socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace, LogLevel.Trace,
new EventId(1028, "SocketProcessingStateChanged"), new EventId(1029, "SocketProcessingStateChanged"),
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}"); "[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( public static void SocketConnecting(
@@ -358,5 +364,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_socketProcessingStateChanged(logger, socketId, prevState, newState, null); _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>( _sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace, LogLevel.Trace,
new EventId(2028, "SendingData"), new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}"); "[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>( _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning, LogLevel.Warning,
+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
}
} }
@@ -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;
} }
} }
@@ -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)
{ {
@@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Requests
_httpClient = CreateClient(proxy, requestTimeout); _httpClient = CreateClient(proxy, requestTimeout);
} }
private HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
{ {
var handler = new HttpClientHandler(); var handler = new HttpClientHandler();
try try
@@ -46,6 +46,10 @@
/// </summary> /// </summary>
public int? PriceDecimals { get; set; } public int? PriceDecimals { get; set; }
/// <summary> /// <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 /// Whether the symbol is currently available for trading
/// </summary> /// </summary>
public bool Trading { get; set; } public bool Trading { get; set; }
@@ -587,15 +587,27 @@ namespace CryptoExchange.Net.Sockets
lock (_receivedMessagesLock) lock (_receivedMessagesLock)
_receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count)); _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 // canceled
break; break;
} }
catch (Exception wse) catch (Exception wse)
{ {
// Connection closed unexpectedly if (!_ctsSource.Token.IsCancellationRequested)
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); // Connection closed unexpectedly
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false) if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
break; break;
+14 -8
View File
@@ -150,9 +150,9 @@ namespace CryptoExchange.Net.Testing
/// </summary> /// </summary>
/// <typeparam name="TClient"></typeparam> /// <typeparam name="TClient"></typeparam>
/// <exception cref="Exception"></exception> /// <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> /// <summary>
@@ -160,26 +160,32 @@ namespace CryptoExchange.Net.Testing
/// </summary> /// </summary>
/// <typeparam name="TClient"></typeparam> /// <typeparam name="TClient"></typeparam>
/// <exception cref="Exception"></exception> /// <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 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")
&& (excludeInterfaces?.Contains(t.Name) != true));
foreach (var clientInterface in clientInterfaces) 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) foreach (var implementation in implementations)
{ {
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()) ?? 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++; methods++;
} }
+17
View File
@@ -66,6 +66,23 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## 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 * Version 8.5.0 - 23 Dec 2024
* Added SetOptions method to update client settings * Added SetOptions method to update client settings
* Added SocketConnection parameter to PeriodicQuery callback * Added SocketConnection parameter to PeriodicQuery callback