mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fe6db589f | |||
| 625dccbbe4 | |||
| e650771d16 | |||
| 3dad28b19d | |||
| 2b9fda985e | |||
| ff8759409b | |||
| 0d9627c13f | |||
| 0179fd7e2a | |||
| b8d0b0cf95 |
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) where T : class
|
||||
{
|
||||
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) where T : class
|
||||
{
|
||||
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)
|
||||
@@ -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);
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<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.5.0</PackageVersion>
|
||||
<AssemblyVersion>8.5.0</AssemblyVersion>
|
||||
<FileVersion>8.5.0</FileVersion>
|
||||
<PackageVersion>8.6.0</PackageVersion>
|
||||
<AssemblyVersion>8.6.0</AssemblyVersion>
|
||||
<FileVersion>8.6.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>
|
||||
|
||||
@@ -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
-1
@@ -169,7 +169,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");
|
||||
|
||||
|
||||
@@ -235,4 +235,18 @@
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Requests
|
||||
_httpClient = CreateClient(proxy, requestTimeout);
|
||||
}
|
||||
|
||||
private HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
|
||||
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
try
|
||||
|
||||
@@ -66,6 +66,14 @@ 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.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
|
||||
|
||||
Reference in New Issue
Block a user