mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-13 09:23:04 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| decef7b137 | |||
| 630f85ec49 | |||
| 9ec4f2276f | |||
| 0a0c66541e | |||
| bb4199620e | |||
| 8a83cd2cb8 | |||
| fcfeaf568f | |||
| 25567ea434 | |||
| 1ab85d4c26 | |||
| be68115099 | |||
| ff0550b0fb | |||
| 1ab1e008fc |
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|||||||
@@ -215,11 +215,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
currentTry++;
|
||||||
var prepareResult = await PrepareAsync(baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
|
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||||
if (!prepareResult)
|
if (!prepareResult)
|
||||||
return new WebCallResult<T>(prepareResult.Error!);
|
return new WebCallResult<T>(prepareResult.Error!);
|
||||||
|
|
||||||
var request = CreateRequest(
|
var request = CreateRequest(
|
||||||
|
requestId,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
definition,
|
definition,
|
||||||
uriParameters,
|
uriParameters,
|
||||||
@@ -249,6 +252,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestId">Request id</param>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
@@ -257,13 +261,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="Exception"></exception>
|
/// <exception cref="Exception"></exception>
|
||||||
protected virtual async Task<CallResult> PrepareAsync(
|
protected virtual async Task<CallResult> PrepareAsync(
|
||||||
|
int requestId,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null)
|
int? weight = null)
|
||||||
{
|
{
|
||||||
var requestId = ExchangeHelpers.NextId();
|
|
||||||
var requestWeight = weight ?? definition.Weight;
|
var requestWeight = weight ?? definition.Weight;
|
||||||
|
|
||||||
// Time sync
|
// Time sync
|
||||||
@@ -305,14 +309,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint specific rate limiting
|
// Endpoint specific rate limiting
|
||||||
if (definition.EndpointLimitCount != null && definition.EndpointLimitPeriod != null)
|
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
if (definition.RateLimitGate == null)
|
if (definition.RateLimitGate == null)
|
||||||
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult(limitResult.Error!);
|
return new CallResult(limitResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -324,6 +328,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a request object
|
/// Creates a request object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestId">Id of the request</param>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="uriParameters">The query parameters of the request</param>
|
/// <param name="uriParameters">The query parameters of the request</param>
|
||||||
@@ -331,6 +336,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IRequest CreateRequest(
|
protected virtual IRequest CreateRequest(
|
||||||
|
int requestId,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? uriParameters,
|
ParameterCollection? uriParameters,
|
||||||
@@ -343,7 +349,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
||||||
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
||||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||||
var requestId = ExchangeHelpers.NextId();
|
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var headers = new Dictionary<string, string>();
|
||||||
@@ -668,47 +673,47 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
|
|
||||||
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(), default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(T) == typeof(object))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||||
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(), default, error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Json response received
|
// Json response received
|
||||||
var parsedError = TryParseError(accessor);
|
var parsedError = TryParseError(accessor);
|
||||||
if (parsedError != null)
|
if (parsedError != null)
|
||||||
// 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(), 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(), 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);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException requestException)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
var exceptionInfo = requestException.ToLogString();
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -962,14 +967,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
if (timeSyncParams == null)
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -998,7 +1003,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldCache(RequestDefinition definition)
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class IntConverter : JsonConverter<int?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class LongConverter : JsonConverter<long?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value).Deserialize(typeof(T));
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
new EnumConverter(),
|
new EnumConverter(),
|
||||||
new BoolConverter(),
|
new BoolConverter(),
|
||||||
new DecimalConverter(),
|
new DecimalConverter(),
|
||||||
|
new IntConverter(),
|
||||||
|
new LongConverter()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>7.7.0</PackageVersion>
|
<PackageVersion>7.8.0</PackageVersion>
|
||||||
<AssemblyVersion>7.7.0</AssemblyVersion>
|
<AssemblyVersion>7.8.0</AssemblyVersion>
|
||||||
<FileVersion>7.7.0</FileVersion>
|
<FileVersion>7.8.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>
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBody"></param>
|
/// <param name="requestBody"></param>
|
||||||
/// <param name="requestMethod"></param>
|
/// <param name="requestMethod"></param>
|
||||||
/// <param name="requestHeaders"></param>
|
/// <param name="requestHeaders"></param>
|
||||||
|
/// <param name="dataSource"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
@@ -362,6 +363,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||||
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
{
|
{
|
||||||
@@ -375,6 +377,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
RequestHeaders = requestHeaders;
|
RequestHeaders = requestHeaders;
|
||||||
RequestMethod = requestMethod;
|
RequestMethod = requestMethod;
|
||||||
|
DataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -398,7 +401,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <param name="error">The error</param>
|
||||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -408,7 +411,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -419,7 +422,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
public new WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -428,9 +431,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal WebCallResult<T> Cached()
|
internal WebCallResult<T> Cached()
|
||||||
{
|
{
|
||||||
var result = new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, Error);
|
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||||
result.DataSource = ResultDataSource.Cache;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -48,18 +48,16 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Request weight
|
/// Request weight
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Weight { get; set; } = 1;
|
public int Weight { get; set; } = 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit gate to use
|
/// Rate limit gate to use
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRateLimitGate? RateLimitGate { get; set; }
|
public IRateLimitGate? RateLimitGate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit for this specific endpoint
|
/// Individual endpoint rate limit guard to use
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EndpointLimitCount { get; set; }
|
public IRateLimitGuard? LimitGuard { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Rate limit period for this specific endpoint
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="method">The HttpMethod</param>
|
/// <param name="method">The HttpMethod</param>
|
||||||
/// <param name="path">Endpoint path</param>
|
/// <param name="path">Endpoint path</param>
|
||||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||||
/// <param name="endpointLimitCount">The limit count for this specific endpoint</param>
|
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
|
||||||
/// <param name="endpointLimitPeriod">The period for the limit for this specific endpoint</param>
|
|
||||||
/// <param name="weight">Request weight</param>
|
/// <param name="weight">Request weight</param>
|
||||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||||
/// <param name="requestBodyFormat">Request body format</param>
|
/// <param name="requestBodyFormat">Request body format</param>
|
||||||
@@ -56,8 +55,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
IRateLimitGate? rateLimitGate,
|
IRateLimitGate? rateLimitGate,
|
||||||
int weight,
|
int weight,
|
||||||
bool authenticated,
|
bool authenticated,
|
||||||
int? endpointLimitCount = null,
|
IRateLimitGuard? limitGuard = null,
|
||||||
TimeSpan? endpointLimitPeriod = null,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
@@ -69,8 +67,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
def = new RequestDefinition(path, method)
|
def = new RequestDefinition(path, method)
|
||||||
{
|
{
|
||||||
Authenticated = authenticated,
|
Authenticated = authenticated,
|
||||||
EndpointLimitCount = endpointLimitCount,
|
LimitGuard = limitGuard,
|
||||||
EndpointLimitPeriod = endpointLimitPeriod,
|
|
||||||
RateLimitGate = rateLimitGate,
|
RateLimitGate = rateLimitGate,
|
||||||
Weight = weight,
|
Weight = weight,
|
||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
|
|||||||
@@ -12,9 +12,22 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SingleLimitGuard : IRateLimitGuard
|
public class SingleLimitGuard : IRateLimitGuard
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default endpoint limit
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, SecureString?, string> Default { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint limit per API key
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
|
||||||
|
|
||||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||||
private readonly RateLimitWindowType _windowType;
|
private readonly RateLimitWindowType _windowType;
|
||||||
private readonly double? _decayRate;
|
private readonly double? _decayRate;
|
||||||
|
private readonly int _limit;
|
||||||
|
private readonly TimeSpan _period;
|
||||||
|
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "EndpointLimitGuard";
|
public string Name => "EndpointLimitGuard";
|
||||||
@@ -25,20 +38,28 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
|
public SingleLimitGuard(
|
||||||
|
int limit,
|
||||||
|
TimeSpan period,
|
||||||
|
RateLimitWindowType windowType,
|
||||||
|
double? decayRate = null,
|
||||||
|
Func<RequestDefinition, string, SecureString?, string>? keySelector = null)
|
||||||
{
|
{
|
||||||
|
_limit = limit;
|
||||||
|
_period = period;
|
||||||
_windowType = windowType;
|
_windowType = windowType;
|
||||||
_decayRate = decayRate;
|
_decayRate = decayRate;
|
||||||
|
_keySelector = keySelector ?? Default;
|
||||||
_trackers = new Dictionary<string, IWindowTracker>();
|
_trackers = new Dictionary<string, IWindowTracker>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
var key = definition.Path + definition.Method;
|
var key = _keySelector(definition, host, apiKey);
|
||||||
if (!_trackers.TryGetValue(key, out var tracker))
|
if (!_trackers.TryGetValue(key, out var tracker))
|
||||||
{
|
{
|
||||||
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
|
tracker = CreateTracker();
|
||||||
_trackers.Add(key, tracker);
|
_trackers.Add(key, tracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,27 +67,27 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
if (delay == default)
|
if (delay == default)
|
||||||
return LimitCheck.NotNeeded;
|
return LimitCheck.NotNeeded;
|
||||||
|
|
||||||
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
var key = definition.Path + definition.Method;
|
var key = _keySelector(definition, host, apiKey);
|
||||||
var tracker = _trackers[key];
|
var tracker = _trackers[key];
|
||||||
tracker.ApplyWeight(requestWeight);
|
tracker.ApplyWeight(requestWeight);
|
||||||
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
return RateLimitState.Applied(_limit, _period, tracker.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new WindowTracker
|
/// Create a new WindowTracker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
|
protected IWindowTracker CreateTracker()
|
||||||
{
|
{
|
||||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
|
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
|
||||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
|
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
|
||||||
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the SingleLimitGuard for handling individual endpoint rate limits
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the 'retry after' timestamp if set
|
/// Returns the 'retry after' timestamp if set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -65,14 +58,14 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="logger">Logger</param>
|
||||||
/// <param name="itemId">Id of the item to check</param>
|
/// <param name="itemId">Id of the item to check</param>
|
||||||
|
/// <param name="guard">The guard</param>
|
||||||
/// <param name="type">The rate limit item type</param>
|
/// <param name="type">The rate limit item type</param>
|
||||||
/// <param name="definition">The request definition</param>
|
/// <param name="definition">The request definition</param>
|
||||||
/// <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="requestWeight">Request weight</param>
|
|
||||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
/// <param name="behaviour">Behaviour when rate limit is hit</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, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class RateLimitGate : IRateLimitGate
|
public class RateLimitGate : IRateLimitGate
|
||||||
{
|
{
|
||||||
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
|
|
||||||
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore;
|
||||||
private readonly string _name;
|
private readonly string _name;
|
||||||
@@ -53,16 +52,23 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
public async Task<CallResult> ProcessSingleAsync(
|
||||||
|
ILogger logger,
|
||||||
|
int itemId,
|
||||||
|
IRateLimitGuard guard,
|
||||||
|
RateLimitItemType type,
|
||||||
|
RequestDefinition definition,
|
||||||
|
string host,
|
||||||
|
SecureString? apiKey,
|
||||||
|
RateLimitingBehaviour rateLimitingBehaviour,
|
||||||
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
if (requestWeight == 0)
|
|
||||||
requestWeight = 1;
|
|
||||||
|
|
||||||
_waitingCount++;
|
_waitingCount++;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -130,13 +136,6 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
|
|
||||||
{
|
|
||||||
_singleLimitGuard = guard;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||||
{
|
{
|
||||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||||
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime()
|
private TimeSpan DetermineWaitTime()
|
||||||
{
|
{
|
||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||||
return wait.Add(_fixedWindowBuffer);
|
var result = wait.Add(_fixedWindowBuffer);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private readonly List<LimitEntry> _entries;
|
private readonly List<LimitEntry> _entries;
|
||||||
private int _currentWeight = 0;
|
private int _currentWeight = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional wait time to apply to account for fluctuating request times
|
||||||
|
/// </summary>
|
||||||
|
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||||
{
|
{
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
removedWeight += entry.Weight;
|
removedWeight += entry.Weight;
|
||||||
if (removedWeight >= weightToRemove)
|
if (removedWeight >= weightToRemove)
|
||||||
{
|
{
|
||||||
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
|
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
{
|
{
|
||||||
var nested = nestedJsonProperty.Split('.');
|
var nested = nestedJsonProperty.Split('.');
|
||||||
foreach (var nest in nested)
|
foreach (var nest in nested)
|
||||||
jsonObject = jsonObject![nest];
|
{
|
||||||
|
if (int.TryParse(nest, out var index))
|
||||||
|
jsonObject = jsonObject![index];
|
||||||
|
else
|
||||||
|
jsonObject = jsonObject![nest];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSingleArrayItem)
|
if (userSingleArrayItem)
|
||||||
@@ -80,6 +85,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
else if (jObj.Type == JTokenType.Array)
|
else if (jObj.Type == JTokenType.Array)
|
||||||
{
|
{
|
||||||
var resultObj = enumerator.Current;
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
@@ -88,9 +97,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var item in jObj.Values())
|
foreach (var item in jObj.Children())
|
||||||
{
|
{
|
||||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
if (arrayProp != null)
|
if (arrayProp != null)
|
||||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
i++;
|
i++;
|
||||||
@@ -108,9 +117,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
{
|
{
|
||||||
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var item in jObjs.Values())
|
foreach (var item in jObjs.Children())
|
||||||
{
|
{
|
||||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
if (arrayProp != null)
|
if (arrayProp != null)
|
||||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
i++;
|
i++;
|
||||||
@@ -224,11 +233,11 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var item in jtoken.Values())
|
foreach (var item in jtoken.Children())
|
||||||
{
|
{
|
||||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
if (arrayProp != null)
|
if (arrayProp != null)
|
||||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@@ -266,7 +275,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
var enumerator = list.GetEnumerator();
|
var enumerator = list.GetEnumerator();
|
||||||
foreach (var jObj in jObjs)
|
foreach (var jObj in jObjs)
|
||||||
{
|
{
|
||||||
enumerator.MoveNext();
|
if (!enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
if (jObj.Type == JTokenType.Object)
|
if (jObj.Type == JTokenType.Object)
|
||||||
{
|
{
|
||||||
foreach (var subProp in ((JObject)jObj).Properties())
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
@@ -307,9 +319,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
{
|
{
|
||||||
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var item in jObjs.Values())
|
foreach (var item in jObjs.Children())
|
||||||
{
|
{
|
||||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
if (arrayProp != null)
|
if (arrayProp != null)
|
||||||
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
i++;
|
i++;
|
||||||
|
|||||||
@@ -26,8 +26,13 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
if (nestedJsonProperty != null)
|
if (nestedJsonProperty != null)
|
||||||
{
|
{
|
||||||
var nested = nestedJsonProperty.Split('.');
|
var nested = nestedJsonProperty.Split('.');
|
||||||
foreach(var nest in nested)
|
foreach (var nest in nested)
|
||||||
jsonObject = jsonObject![nest];
|
{
|
||||||
|
if (int.TryParse(nest, out var index))
|
||||||
|
jsonObject = jsonObject![index];
|
||||||
|
else
|
||||||
|
jsonObject = jsonObject![nest];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSingleArrayItem)
|
if (userSingleArrayItem)
|
||||||
@@ -65,44 +70,62 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
else if (jsonObject!.Type == JTokenType.Array)
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
{
|
{
|
||||||
var jObjs = (JArray)jsonObject;
|
var jObjs = (JArray)jsonObject;
|
||||||
var list = (IEnumerable)resultData;
|
if (resultData is IEnumerable list)
|
||||||
var enumerator = list.GetEnumerator();
|
|
||||||
foreach (var jObj in jObjs)
|
|
||||||
{
|
{
|
||||||
enumerator.MoveNext();
|
var enumerator = list.GetEnumerator();
|
||||||
if (jObj.Type == JTokenType.Object)
|
foreach (var jObj in jObjs)
|
||||||
{
|
{
|
||||||
foreach (var subProp in ((JObject)jObj).Properties())
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
{
|
{
|
||||||
if (ignoreProperties?.Contains(subProp.Name) == true)
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
continue;
|
continue;
|
||||||
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (jObj.Type == JTokenType.Array)
|
|
||||||
{
|
|
||||||
var resultObj = enumerator.Current;
|
|
||||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
|
||||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
|
||||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
|
||||||
if (jsonConverter != typeof(ArrayConverter))
|
|
||||||
// Not array converter?
|
|
||||||
continue;
|
|
||||||
|
|
||||||
int i = 0;
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
foreach (var item in jObj.Values())
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
var value = enumerator.Current;
|
||||||
if (arrayProp != null)
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Children())
|
||||||
{
|
{
|
||||||
var value = enumerator.Current;
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
if (arrayProp != null)
|
||||||
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,9 +238,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (var item in jtoken.Values())
|
foreach (var item in jtoken.Children())
|
||||||
{
|
{
|
||||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
if (arrayProp != null)
|
if (arrayProp != null)
|
||||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,22 @@ 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 7.8.0 - 02 Jul 2024
|
||||||
|
* Updated single endpoint limit configuration
|
||||||
|
* Added LongConverter for nullable longs
|
||||||
|
* Updated SystemTextJsonComparer logic
|
||||||
|
|
||||||
|
* Version 7.7.3 - 26 Jun 2024
|
||||||
|
* Fixed request ids not matching in logging
|
||||||
|
* Added nullable int converter for System.Text.Json
|
||||||
|
* Small fixes in tests
|
||||||
|
|
||||||
|
* Version 7.7.2 - 25 Jun 2024
|
||||||
|
* Fixed ratelimiting issue possibly creating negative delays
|
||||||
|
|
||||||
|
* Version 7.7.1 - 23 Jun 2024
|
||||||
|
* Fixes for caching implementation
|
||||||
|
|
||||||
* Version 7.7.0 - 23 Jun 2024
|
* Version 7.7.0 - 23 Jun 2024
|
||||||
* Caching support
|
* Caching support
|
||||||
* Caching is supported for GET requests within a certain time frame
|
* Caching is supported for GET requests within a certain time frame
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
<li class="nav-item"><a class="nav-link" href="#idocs_orderbooks">Orderbooks</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_orderbooks">Orderbooks</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_logging">Logging</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_logging">Logging</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_ratelimiting">Ratelimiting</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_ratelimiting">Ratelimiting</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#idocs_caching">Caching</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_examples">Examples</a>
|
<li class="nav-item"><a class="nav-link" href="#idocs_examples">Examples</a>
|
||||||
@@ -1956,6 +1957,16 @@ var client = new OKXRestClient();</code></pre>
|
|||||||
<td>The interval of how often the time synchronization between client and server should be executed</td>
|
<td>The interval of how often the time synchronization between client and server should be executed</td>
|
||||||
<td><code>TimeSpan.FromHours(1)</code></td>
|
<td><code>TimeSpan.FromHours(1)</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>CachingEnabled</td>
|
||||||
|
<td>Whether or not client side caching should be enabled for GET requests, see <a href="#idocs_caching">Caching</a></td>
|
||||||
|
<td><code>false</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>CachingMaxAge</td>
|
||||||
|
<td>The max age of data to return from the cache. If the same data is requested and the data is available in the client side cache and not older than this value the cached value is returned, else a new request will be done</td>
|
||||||
|
<td><code>TimeSpan.FromSeconds(5)</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>[API].ApiCredentials</td>
|
<td>[API].ApiCredentials</td>
|
||||||
<td>Same as the in the base options, allows overriding per sub-API</td>
|
<td>Same as the in the base options, allows overriding per sub-API</td>
|
||||||
@@ -2474,6 +2485,30 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="idocs_caching">
|
||||||
|
<h2>Caching</h2>
|
||||||
|
<p>
|
||||||
|
Every REST API client based on the CryptoExchange.Net base library automatically supports caching of GET HTTP requests. A few advantages of caching:
|
||||||
|
<ol>
|
||||||
|
<li>Performance improvement, data response will be much faster as no roundtrip to the server is needed</li>
|
||||||
|
<li>Reduced resource usage, returning data from the cache uses less resources than reading the server response, though there is some memory overhead</li>
|
||||||
|
<li>Prevent rate limiting, the cache can be queried as many times as you like without having to worry about getting rate limited by the server</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="alert alert-info">Caching is only applied for successful GET requests as GET requests by definition should not change state. Other HTTP method (POST, DELETE, etc) generally do change state, so caching those call would prevent an action being executed.</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To enable caching for GET requests set <code>CachingEnabled</code> to <code>true</code> in the client options. Optionally set the <code>CachingMaxAge</code> option to the desired value (default is 5 seconds).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To determine whether a request has gotten the data from the server or from the local cache the <code>DataSource</code> property on the call result can inspected:
|
||||||
|
<pre><code>var result = await bitfinexRestClient.SpotApi.Account.Get30DaySummaryAndFeesAsync();
|
||||||
|
var responseSource = result.DataSource;</code></pre>
|
||||||
|
</p>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr class="divider">
|
<hr class="divider">
|
||||||
|
|||||||
Reference in New Issue
Block a user