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

Compare commits

...

27 Commits

Author SHA1 Message Date
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
Jkorf 73c42bd452 Updated to version 8.5.0 2024-12-23 14:25:39 +01:00
Jkorf 290be7f5e0 Added net9.0 build target, added KeepAliveTimeout for websocket connections 2024-12-23 14:14:47 +01:00
Jan Korf 0be1bb16e3 Feature/update settings (#225)
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
2024-12-23 08:49:58 +01:00
Jkorf 8605196390 Updated to version 8.4.5 2024-12-20 15:25:41 +01:00
Jkorf 460dd97537 Added EmptyArrayObjectConverter, added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor ctor 2024-12-20 15:21:21 +01:00
JKorf 1ec5984fad Updated to version 8.4.4 2024-12-08 10:16:19 +01:00
JKorf 8260c2661d Changed JsonConverterCtorAttribute to use type parameter instead of generic parameter to support .net framework 2024-12-08 10:12:39 +01:00
Jkorf 591c1dd405 docs 2024-12-04 10:44:35 +01:00
Jkorf 0164cdfcc4 docs 2024-12-04 09:15:23 +01:00
Jkorf 23a6cfff87 docs 2024-12-04 09:07:59 +01:00
Jkorf fdcdb90a5f Added XT reference 2024-12-04 08:58:46 +01:00
Jkorf 0b7107401f Updated to version 8.4.3 2024-12-03 09:59:36 +01:00
JKorf 06add65354 Fixed KlineTracker update handling 2024-12-02 21:46:30 +01:00
Jkorf 773d288497 Updated to version 8.4.2 2024-12-02 14:39:27 +01:00
Jkorf fd4e8da938 Removed special characters in ClientOrderIdSeperator to adhere to field content rules 2024-12-02 14:38:22 +01:00
59 changed files with 774 additions and 152 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>
@@ -73,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests
} }
[Test] [Test]
[TestCase("123", "BKR", 32, true, "BKR|JK|123")] [TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")] [TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30 [TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123")] // 27 [TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("1231231231231231231231231", "BKR", 32, true, "BKR|JK|1231231231231231231231231")] // 25 [TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)] [TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected) public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{ {
@@ -1,4 +1,4 @@
#if !NETSTANDARD2_1 #if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis namespace System.Diagnostics.CodeAnalysis
{ {
using System; using System;
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create(); var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem) if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{ {
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key // Read from pem private key
var key = _credentials.Secret! var key = _credentials.Secret!
.Replace("\n", "") .Replace("\n", "")
@@ -403,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static string BytesToHexString(byte[] buff) protected static string BytesToHexString(byte[] buff)
{ {
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty; var result = string.Empty;
foreach (var t in buff) foreach (var t in buff)
result += t.ToString("X2"); result += t.ToString("X2");
return result; return result;
#endif
} }
/// <summary> /// <summary>
+1 -1
View File
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns> /// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge) public object? Get(string key, TimeSpan maxAge)
{ {
_cache.TryGetValue(key, out CacheItem value); _cache.TryGetValue(key, out CacheItem? value);
if (value == null) if (value == null)
return null; return null;
@@ -93,6 +93,17 @@ namespace CryptoExchange.Net.Clients
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
} }
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+2 -2
View File
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Version of the CryptoExchange.Net base library /// Version of the CryptoExchange.Net base library
/// </summary> /// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version; public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary> /// <summary>
/// Version of the client implementation /// Version of the client implementation
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Clients
lock(_versionLock) lock(_versionLock)
{ {
if (_exchangeVersion == null) if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version; _exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion; return _exchangeVersion;
} }
+43 -16
View File
@@ -154,6 +154,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>( protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -161,7 +162,8 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? parameters, ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null,
int? weightSingleLimiter = null)
{ {
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)
@@ -807,7 +826,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InUri) if (parameterPosition == HttpMethodParameterPosition.InUri)
{ {
foreach (var parameter in parameters) foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()); uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()!);
} }
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
@@ -889,8 +908,8 @@ namespace CryptoExchange.Net.Clients
{ {
// Write the parameters as json in the body // Write the parameters as json in the body
string stringData; string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey)) if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]); stringData = CreateSerializer().Serialize(value);
else else
stringData = CreateSerializer().Serialize(parameters); stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType); request.SetContent(stringData, contentType);
@@ -961,6 +980,14 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns> /// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
+22 -2
View File
@@ -158,7 +158,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -422,9 +422,10 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message; result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!; return new CallResult(result.Error)!;
} }
_logger.Authenticated(socket.SocketId);
} }
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true; socket.Authenticated = true;
return new CallResult(null); return new CallResult(null);
} }
@@ -710,6 +711,25 @@ namespace CryptoExchange.Net.Clients
return new CallResult(null); return new CallResult(null);
} }
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| !socketConnections.Any())
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
var result = Activator.CreateInstance(objectType); var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader); var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType); return ParseObject(arr, result!, objectType);
} }
private static object ParseObject(JArray arr, object result, Type objectType) private static object ParseObject(JArray arr, object result, Type objectType)
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
var count = 0; var count = 0;
if (innerArray.Count == 0) if (innerArray.Count == 0)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else if (innerArray[0].Type == JTokenType.Array) else if (innerArray[0].Type == JTokenType.Array)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
foreach (var obj in innerArray) foreach (var obj in innerArray)
{ {
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!); arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
count++; count++;
} }
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else else
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!); arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
continue; continue;
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
object? value; object? value;
if (converterAttribute != null) if (converterAttribute != null)
{ {
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
} }
else if (conversionAttribute != null) else if (conversionAttribute != null)
{ {
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
else if ((property.PropertyType == typeof(decimal) else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?)) || property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0)) && (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
var v = value.ToString(); var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec)) if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
last = arrayProp.Index; last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop); var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null) if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType))); writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
else if (!IsSimple(prop.PropertyType)) else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value)); serializer.Serialize(writer, prop.GetValue(value));
else else
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute => private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))); (T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
private static T? GetCustomAttribute<T>(Type type) where T : Attribute => private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))); (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
} }
} }
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
{ {
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
var value = reader.Value!.ToString(); var value = reader.Value!.ToString()!;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
/// </returns> /// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var value = reader.Value?.ToString().ToLower().Trim(); var value = reader.Value?.ToString()!.ToLower().Trim();
if (value == null || value == "") if (value == null || value == "")
{ {
if (Nullable.GetUnderlyingType(objectType) != null) if (Nullable.GetUnderlyingType(objectType) != null)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader); using var jsonTextReader = new JsonTextReader(reader);
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class ArrayPropertyInfo private class ArrayPropertyInfo
@@ -79,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonSerializerOptions? typeOptions = null; JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null) if (prop.JsonConverterType != null)
{ {
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
typeOptions = new JsonSerializerOptions(); typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear(); typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter); typeOptions.Converters.Add(converter);
@@ -90,7 +90,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (prop.PropertyInfo.PropertyType == typeof(string)) if (prop.PropertyInfo.PropertyType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
} }
else else
{ {
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
return default; return default;
var result = Activator.CreateInstance(typeToConvert); var result = Activator.CreateInstance(typeToConvert)!;
return (T)ParseObject(ref reader, result, typeToConvert, options); return (T)ParseObject(ref reader, result, typeToConvert, options);
} }
@@ -177,7 +177,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
{ {
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
newOptions = new JsonSerializerOptions newOptions = new JsonSerializerOptions
{ {
NumberHandling = SerializerOptions.WithConverters.NumberHandling, NumberHandling = SerializerOptions.WithConverters.NumberHandling,
@@ -209,7 +209,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value == null ? null : value); attribute.PropertyInfo.SetValue(result, value);
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch(OverflowException) catch(OverflowException)
{ {
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class BoolConverterInner<T> : JsonConverter<T> private class BoolConverterInner<T> : JsonConverter<T>
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class DateTimeConverterInner<T> : JsonConverter<T> private class DateTimeConverterInner<T> : JsonConverter<T>
@@ -74,7 +74,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
{
writer.WriteNullValue(); writer.WriteNullValue();
}
else else
{ {
var dtValue = (DateTime)(object)value; var dtValue = (DateTime)(object)value;
@@ -0,0 +1,37 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter mapping to an object but also handles when an empty array is send
/// </summary>
/// <typeparam name="T"></typeparam>
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
{
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
/// <inheritdoc />
public override T? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.StartArray:
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
return default;
case JsonTokenType.StartObject:
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
};
return default;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, (object?)value, options);
}
}
@@ -6,21 +6,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary> /// <summary>
/// Attribute for allowing specifying a JsonConverter with constructor parameters /// Attribute for allowing specifying a JsonConverter with constructor parameters
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class JsonConverterCtorAttribute<T> : JsonConverterAttribute where T : JsonConverter public class JsonConverterCtorAttribute : JsonConverterAttribute
{ {
private readonly object[] _parameters; private readonly object[] _parameters;
private readonly Type _type;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public JsonConverterCtorAttribute(params object[] parameters) => _parameters = parameters; public JsonConverterCtorAttribute(Type type, params object[] parameters)
{
_type = type;
_parameters = parameters;
}
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert) public override JsonConverter CreateConverter(Type typeToConvert)
{ {
return (T)Activator.CreateInstance(typeof(T), _parameters); return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
} }
} }
@@ -21,6 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
protected JsonDocument? _document; protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc /> /// <inheritdoc />
public bool IsJson { get; set; } public bool IsJson { get; set; }
@@ -31,6 +32,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public object? Underlying => throw new NotImplementedException(); public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null) public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{ {
@@ -42,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize(type, _serializerOptions); var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
return new CallResult<object>(result!); return new CallResult<object>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -65,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize<T>(_serializerOptions); var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
return new CallResult<T>(result!); return new CallResult<T>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -129,7 +145,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return value.Value.Deserialize<T>(_serializerOptions); return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
} }
catch { } catch { }
return default; return default;
@@ -223,6 +239,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true; public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(): base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream) public async Task<CallResult> Read(Stream stream, bool bufferStream)
{ {
@@ -286,6 +316,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private ReadOnlyMemory<byte> _bytes; private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor() : base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data) public CallResult Read(ReadOnlyMemory<byte> data)
{ {
+4 -4
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>8.4.1</PackageVersion> <PackageVersion>8.6.1</PackageVersion>
<AssemblyVersion>8.4.1</AssemblyVersion> <AssemblyVersion>8.6.1</AssemblyVersion>
<FileVersion>8.4.1</FileVersion> <FileVersion>8.6.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
+1 -1
View File
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net
{ {
var randomChars = new char[length]; var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else #else
+5 -3
View File
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&"; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
} }
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
@@ -111,7 +111,7 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
} }
} }
return formData.ToString(); return formData.ToString()!;
} }
/// <summary> /// <summary>
@@ -366,7 +366,7 @@ namespace CryptoExchange.Net
{ {
using var decompressedStream = new MemoryStream(); using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream); deflateStream.CopyTo(decompressedStream);
@@ -435,6 +435,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IWithdrawalRestClient)client(x)!); services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T))) if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawRestClient)client(x)!); services.AddTransient(x => (IWithdrawRestClient)client(x)!);
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!); services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
@@ -31,5 +32,12 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="credentials"></param> /// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials; void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api crentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
} }
} }
@@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param> /// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param> /// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null); void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
} }
} }
@@ -93,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task CloseAsync(); Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@ namespace CryptoExchange.Net
/// <summary> /// <summary>
/// Client order id seperator /// Client order id seperator
/// </summary> /// </summary>
public const string ClientOrderIdSeperator = "|JK|"; public const string ClientOrderIdSeperator = "JK";
/// <summary> /// <summary>
/// Apply broker id to a client order id /// Apply broker id to a client order id
@@ -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);
}
} }
} }
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(byte[] x, byte[] y) public int Compare(byte[]? x, byte[]? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
+14
View File
@@ -235,4 +235,18 @@
Cache Cache
} }
/// <summary>
/// Type of exchange
/// </summary>
public enum ExchangeType
{
/// <summary>
/// Centralized
/// </summary>
CEX,
/// <summary>
/// Decentralized
/// </summary>
DEX
}
} }
@@ -0,0 +1,29 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<T> where T : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public T? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}
/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
}
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(string x, string y) public int Compare(string? x, string? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
@@ -156,7 +156,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddSecondsString(string key, DateTime value) public void AddSecondsString(string key, DateTime value)
{ {
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.Objects
public void AddOptionalSecondsString(string key, DateTime? value) public void AddOptionalSecondsString(string key, DateTime? value)
{ {
if (value != null) if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -187,7 +187,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value) public void AddEnumAsInt<T>(string key, T value)
{ {
var stringVal = EnumConverter.GetString(value); var stringVal = EnumConverter.GetString(value)!;
Add(key, int.Parse(stringVal)!); Add(key, int.Parse(stringVal)!);
} }
@@ -58,9 +58,38 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{ {
if (!_definitions.TryGetValue(method + path, out var def)) if (!_definitions.TryGetValue(identifier, out var def))
{ {
def = new RequestDefinition(path, method) def = new RequestDefinition(path, method)
{ {
@@ -73,7 +102,7 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false PreventCaching = preventCaching ?? false
}; };
_definitions.TryAdd(method + path, def); _definitions.TryAdd(identifier, def);
} }
return def; return def;
+2 -2
View File
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow; TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{ {
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms"); Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
TimeSyncState.TimeOffset = TimeSpan.Zero; TimeSyncState.TimeOffset = TimeSpan.Zero;
} }
else else
{ {
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms"); Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
TimeSyncState.TimeOffset = offset; TimeSyncState.TimeOffset = offset;
} }
} }
@@ -843,9 +843,9 @@ namespace CryptoExchange.Net.OrderBook
internal class DescComparer<T> : IComparer<T> internal class DescComparer<T> : IComparer<T>
{ {
public int Compare(T x, T y) public int Compare(T? x, T? y)
{ {
return Comparer<T>.Default.Compare(y, x); return Comparer<T>.Default.Compare(y!, x!);
} }
} }
} }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Apply guard per connection /// Apply guard per connection
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()); public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
/// <summary> /// <summary>
/// Apply guard per API key /// Apply guard per API key
/// </summary> /// </summary>
@@ -68,8 +68,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="baseAddress">The host address</param> /// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct); Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
} }
} }
@@ -69,6 +69,7 @@ namespace CryptoExchange.Net.RateLimiting
RequestDefinition definition, RequestDefinition definition,
string host, string host,
string? apiKey, string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour, RateLimitingBehaviour rateLimitingBehaviour,
CancellationToken ct) CancellationToken ct)
{ {
@@ -77,7 +78,7 @@ namespace CryptoExchange.Net.RateLimiting
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
+1 -1
View File
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.Requests
} }
/// <inheritdoc /> /// <inheritdoc />
public Uri Uri => _request.RequestUri; public Uri Uri => _request.RequestUri!;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
+33 -22
View File
@@ -17,28 +17,7 @@ namespace CryptoExchange.Net.Requests
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
if (client == null) if (client == null)
{ client = CreateClient(proxy, requestTimeout);
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
_httpClient = client; _httpClient = client;
} }
@@ -51,5 +30,37 @@ namespace CryptoExchange.Net.Requests
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
} }
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
{
_httpClient = CreateClient(proxy, requestTimeout);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var client = new HttpClient(handler)
{
Timeout = requestTimeout
};
return client;
}
} }
} }
@@ -60,8 +60,8 @@ namespace CryptoExchange.Net.SharedApis
} }
else else
{ {
if (param.Names.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -113,13 +113,13 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (!string.IsNullOrEmpty(param.Name)) if (!string.IsNullOrEmpty(param.Name))
{ {
if (typeof(T).GetProperty(param.Name).GetValue(request, null) == null) if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
} }
else else
{ {
if (param.Names.All(x => typeof(T).GetProperty(param.Name).GetValue(request, null) == null)) if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
public override string ToString(string exchange) public override string ToString(string exchange)
{ {
var sb = new StringBuilder(base.ToString(exchange)); var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Supported limit values: [{(SupportedLimits == null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (Name != null) if (Name != null)
return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}";
return $"[{ValueType.Name}] {string.Join(" / ", Names)}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}";
} }
} }
} }
@@ -155,6 +155,12 @@ namespace CryptoExchange.Net.Sockets
_baseAddress = $"{Uri.Scheme}://{Uri.Host}"; _baseAddress = $"{Uri.Scheme}://{Uri.Host}";
} }
/// <inheritdoc />
public void UpdateProxy(ApiProxy? proxy)
{
Parameters.Proxy = proxy;
}
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<CallResult> ConnectAsync() public virtual async Task<CallResult> ConnectAsync()
{ {
@@ -189,9 +195,12 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null) if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy); SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true; socket.Options.CollectHttpResponseDetails = true;
#endif #endif
#if NET9_0_OR_GREATER
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
#endif
} }
catch (PlatformNotSupportedException) catch (PlatformNotSupportedException)
{ {
@@ -229,13 +238,13 @@ namespace CryptoExchange.Net.Sockets
if (e is WebSocketException we) if (e is WebSocketException we)
{ {
#if (NET6_0_OR_GREATER) #if (NET6_0_OR_GREATER)
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
{ {
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#else #else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead // Try to read 429 from the message instead
if (we.Message.Contains("429")) if (we.Message.Contains("429"))
@@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#endif #endif
} }
return new CallResult(new CantConnectError()); return new CallResult(new CantConnectError());
@@ -435,8 +444,8 @@ namespace CryptoExchange.Net.Sockets
{ {
// Wait until we receive close confirmation // Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false); await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5)) if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
break; // Wait for max 5 seconds, then just abort the connection break; // Wait for max 1 second, then just abort the connection
} }
} }
} }
@@ -578,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;
@@ -598,14 +619,14 @@ namespace CryptoExchange.Net.Sockets
if (_socket.State == WebSocketState.CloseReceived) if (_socket.State == WebSocketState.CloseReceived)
{ {
// Close received means it server initiated, we should send a confirmation and close the socket // Close received means it server initiated, we should send a confirmation and close the socket
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
if (_closeTask?.IsCompleted != false) if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
} }
else else
{ {
// Means the socket is now closed and we were the one initiating it // Means the socket is now closed and we were the one initiating it
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
} }
break; break;
@@ -620,7 +641,7 @@ namespace CryptoExchange.Net.Sockets
// Write the data to a memory stream to be reassembled later // Write the data to a memory stream to be reassembled later
if (multipartStream == null) if (multipartStream == null)
multipartStream = new MemoryStream(); multipartStream = new MemoryStream();
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
else else
{ {
@@ -634,7 +655,7 @@ namespace CryptoExchange.Net.Sockets
{ {
// Received the end of a multipart message, write to memory stream for reassembling // Received the end of a multipart message, write to memory stream for reassembling
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count); _logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
break; break;
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Callback after query /// Callback after query
/// </summary> /// </summary>
public Action<CallResult>? Callback { get; set; } public Action<SocketConnection, CallResult>? Callback { get; set; }
} }
} }
+5
View File
@@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool Completed { get; set; } public bool Completed { get; set; }
/// <summary>
/// Timeout for the request
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
/// <summary> /// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response /// and each symbol receives it's own confirmation response
+17 -5
View File
@@ -11,6 +11,8 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using System.Threading; using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -396,7 +398,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId) protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -425,7 +427,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="requestId">Id of the request sent</param> /// <param name="requestId">Id of the request sent</param>
protected virtual Task HandleRequestSentAsync(int requestId) protected virtual Task HandleRequestSentAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -437,7 +439,7 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask; return Task.CompletedTask;
} }
query.IsSend(ApiClient.ClientOptions.RequestTimeout); query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -583,6 +585,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
/// <summary>
/// Update the proxy setting and reconnect
/// </summary>
/// <param name="proxy">New proxy setting</param>
public async Task UpdateProxy(ApiProxy? proxy)
{
_socket.UpdateProxy(proxy);
await TriggerReconnectAsync().ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Close the connection /// Close the connection
/// </summary> /// </summary>
@@ -988,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="interval">How often</param> /// <param name="interval">How often</param>
/// <param name="queryDelegate">Method returning the query to send</param> /// <param name="queryDelegate">Method returning the query to send</param>
/// <param name="callback">The callback for processing the response</param> /// <param name="callback">The callback for processing the response</param>
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
if (queryDelegate == null) if (queryDelegate == null)
throw new ArgumentNullException(nameof(queryDelegate)); throw new ArgumentNullException(nameof(queryDelegate));
@@ -1020,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
callback?.Invoke(result); callback?.Invoke(this, result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -378,7 +378,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal) if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString())) if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
} }
} }
@@ -211,7 +211,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object) if (dictProp.Value.Type == JTokenType.Object)
{ {
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties); CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties);
} }
else else
{ {
@@ -5,8 +5,11 @@ namespace CryptoExchange.Net.Testing
{ {
internal class EnumValueTraceListener : TraceListener internal class EnumValueTraceListener : TraceListener
{ {
public override void Write(string message) public override void Write(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -14,8 +17,11 @@ namespace CryptoExchange.Net.Testing
throw new Exception("Enum null error: " + message); throw new Exception("Enum null error: " + message);
} }
public override void WriteLine(string message) public override void WriteLine(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -25,5 +25,7 @@ namespace CryptoExchange.Net.Testing.Implementations
_request.RequestId = requestId; _request.RequestId = requestId;
return _request; return _request;
} }
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
} }
} }
@@ -92,5 +92,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public Task ReconnectAsync() => throw new NotImplementedException(); public Task ReconnectAsync() => throw new NotImplementedException();
public void Dispose() { } public void Dispose() { }
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
} }
} }
+2 -4
View File
@@ -169,7 +169,7 @@ namespace CryptoExchange.Net.Testing
{ {
var assembly = Assembly.GetAssembly(clientType); var assembly = Assembly.GetAssembly(clientType);
var interfaceType = clientType.GetInterface("I" + clientType.Name); var interfaceType = clientType.GetInterface("I" + clientType.Name);
var clientInterfaces = assembly.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared")); var clientInterfaces = assembly!.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared"));
foreach (var clientInterface in clientInterfaces) foreach (var clientInterface in clientInterfaces)
{ {
@@ -179,9 +179,7 @@ namespace CryptoExchange.Net.Testing
int methods = 0; int methods = 0;
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType))) foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
{ {
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()); var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) ?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
if (interfaceMethod == null)
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
methods++; methods++;
} }
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Trackers.Klines
/// <summary> /// <summary>
/// The internal data structure /// The internal data structure
/// </summary> /// </summary>
protected readonly Dictionary<DateTime, SharedKline> _data = new Dictionary<DateTime, SharedKline>(); protected readonly SortedDictionary<DateTime, SharedKline> _data = new SortedDictionary<DateTime, SharedKline>();
/// <summary> /// <summary>
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
/// </summary> /// </summary>
@@ -350,7 +350,8 @@ namespace CryptoExchange.Net.Trackers.Trades
_data.Add(item); _data.Add(item);
} }
_firstTimestamp = _data.Min(v => v.Timestamp); if (_data.Any())
_firstTimestamp = _data.Min(v => v.Timestamp);
ApplyWindow(false); ApplyWindow(false);
} }
+52
View File
@@ -29,6 +29,7 @@ The following API's are directly supported. Note that there are 3rd party implem
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)| |Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)| |OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|
|WhiteBit|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)| |WhiteBit|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|
|XT|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's. Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
@@ -39,6 +40,21 @@ A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free t
## Support the project ## Support the project
Any support is greatly appreciated. Any support is greatly appreciated.
## Referral
When creating an account on new exchanges please consider using a referral link from below to support development
|Exchange|Link|
|--|--|
|Bybit|[https://partner.bybit.com/b/jkorf](https://partner.bybit.com/b/jkorf)|
|Coinbase|[https://advanced.coinbase.com/join/T6H54H8](https://advanced.coinbase.com/join/T6H54H8)|
|CoinEx|[https://www.coinex.com/register?refer_code=hd6gn](https://www.coinex.com/register?refer_code=hd6gn)|
|Crypto.com|[https://crypto.com/exch/26ge92xbkn](https://crypto.com/exch/26ge92xbkn)|
|HTX|[https://www.htx.com/invite/en-us/1f?invite_code=fxp9](https://www.htx.com/invite/en-us/1f?invite_code=fxp9)|
|Kucoin|[https://www.kucoin.com/r/rf/QBS4FPED](https://www.kucoin.com/r/rf/QBS4FPED)|
|OKX|[https://okx.com/join/48046699](https://okx.com/join/48046699)|
|WhiteBit|[https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|
|XT|[https://www.xt.com/en/accounts/register?ref=1HRM5J](https://www.xt.com/en/accounts/register?ref=1HRM5J)|
### Donate ### Donate
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me. Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
@@ -50,6 +66,42 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 8.6.1 - 09 Jan 2025
* Fixed websocket connection getting stuck after a ping frame timeout
* Removed websocket Error callback when exception is expected
* Removed unnecessary type restraints on RestApiClient.SendAsync methods
* Version 8.6.0 - 07 Jan 2025
* Added support for passing weight to apply to an individual ratelimit guard
* Added IFeeRestClient to service registration
* Added response headers parameter to RestApiClient.TryParseError method
* Added check for ServerRateLimitError on RestApiClient.TryParseError response
* Added ExchangeType Enum
* Some small improvements
* Version 8.5.0 - 23 Dec 2024
* Added SetOptions method to update client settings
* Added SocketConnection parameter to PeriodicQuery callback
* Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
* Added support for overriding request timeout per request
* Added build target for net9.0
* Added setting of KeepAliveTimeout on websocket connections to improve dropped connection detection
* Changed max wait time for close handshake response from 5 seconds to 1 second
* Fixed exception in trade tracker when there is no data in the initial snapshot
* Version 8.4.5 - 20 Dec 2024
* Added EmptyArrayObjectConverter System.Text.Json JsonConverter
* Added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor constructor
* Version 8.4.4 - 08 Dec 2024
* Changed JsonConverterCtorAttribute to use constructor type parameter instead of generic type parameter to support .net framework
* Version 8.4.3 - 03 Dec 2024
* Fixed KlineTracker update handling
* Version 8.4.2 - 02 Dec 2024
* Removed special characters in ClientOrderIdSeperator to adhere to field content rules
* Version 8.4.1 - 02 Dec 2024 * Version 8.4.1 - 02 Dec 2024
* Added JsonConverterCtorAttribute to allow specifying a custom JsonConverter with constructor parameters on properties * Added JsonConverterCtorAttribute to allow specifying a custom JsonConverter with constructor parameters on properties
* Added ReplaceConverter System.Text.Json converter * Added ReplaceConverter System.Text.Json converter
+262 -7
View File
@@ -163,6 +163,7 @@
<tr><td>Mexc</td><td><a href="https://github.com/JKorf/Mexc.Net">JKorf/Mexc.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Mexc.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr> <tr><td>Mexc</td><td><a href="https://github.com/JKorf/Mexc.Net">JKorf/Mexc.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Mexc.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr>
<tr><td>OKX</td><td><a href="https://github.com/JKorf/OKX.Net">JKorf/OKX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.OKX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square" /></a></td></tr> <tr><td>OKX</td><td><a href="https://github.com/JKorf/OKX.Net">JKorf/OKX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.OKX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square" /></a></td></tr>
<tr><td>WhiteBit</td><td><a href="https://github.com/JKorf/WhiteBit.Net">JKorf/WhiteBit.Net</a></td><td><a href="https://www.nuget.org/packages/WhiteBit.Net" target="_blank"><img src="https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square" /></a></td></tr> <tr><td>WhiteBit</td><td><a href="https://github.com/JKorf/WhiteBit.Net">JKorf/WhiteBit.Net</a></td><td><a href="https://www.nuget.org/packages/WhiteBit.Net" target="_blank"><img src="https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square" /></a></td></tr>
<tr><td>XT</td><td><a href="https://github.com/JKorf/XT.Net">JKorf/XT.Net</a></td><td><a href="https://www.nuget.org/packages/XT.Net" target="_blank"><img src="https://img.shields.io/nuget/v/XT.net.svg?style=flat-square" /></a></td></tr>
</table> </table>
<p>Note that there are 3rd party implementations going around, but only the listed ones here are created and supported by me.</p> <p>Note that there are 3rd party implementations going around, but only the listed ones here are created and supported by me.</p>
<p>When using multiple of these API's the <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> package can be used which combines these packages and allows easy access to all exchange API's.</p> <p>When using multiple of these API's the <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> package can be used which combines these packages and allows easy access to all exchange API's.</p>
@@ -194,6 +195,21 @@
<h4>Support the project</h4> <h4>Support the project</h4>
<b>Referral</b>
<p>When creating an account on new exchanges please consider using a referral link from below to support development</p>
<table>
<tr><td>Exchange</td><td>Link</td></tr>
<tr><td>Bybit</td><td>https://partner.bybit.com/b/jkorf</td></tr>
<tr><td>Coinbase</td><td>https://advanced.coinbase.com/join/T6H54H8</td></tr>
<tr><td>CoinEx</td><td>https://www.coinex.com/register?refer_code=hd6gn</td></tr>
<tr><td>Crypto.com</td><td>https://crypto.com/exch/26ge92xbkn</td></tr>
<tr><td>HTX</td><td>https://www.htx.com/invite/en-us/1f?invite_code=fxp9</td></tr>
<tr><td>Kucoin</td><td>https://www.kucoin.com/r/rf/QBS4FPED</td></tr>
<tr><td>OKX</td><td>https://okx.com/join/48046699</td></tr>
<tr><td>WhiteBit</td><td>https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf</td></tr>
<tr><td>XT</td><td>https://www.xt.com/en/accounts/register?ref=1HRM5J</td></tr>
</table>
<b>Donate</b><br /> <b>Donate</b><br />
<p>Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.<p> <p>Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.<p>
@@ -203,9 +219,7 @@
<b>Sponsor</b><br /> <b>Sponsor</b><br />
<p>Alternatively, sponsor me on Github using <a href="https://github.com/sponsors/JKorf">Github Sponsors</a>.</p> <p>Alternatively, sponsor me on Github using <a href="https://github.com/sponsors/JKorf">Github Sponsors</a>.</p>
<div class="alert alert-info">I develop and maintain these packages on my own for free in my spare time, any support is greatly appreciated.</div>
</section> </section>
<hr class="divider"> <hr class="divider">
@@ -279,7 +293,10 @@
<a class="nav-link" id="install-okx-tab" data-toggle="tab" href="#install-okx" role="tab" aria-controls="install-okx" aria-selected="false">OKX</a> <a class="nav-link" id="install-okx-tab" data-toggle="tab" href="#install-okx" role="tab" aria-controls="install-okx" aria-selected="false">OKX</a>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="install-whitebit-tab" data-toggle="tab" href="#install-whitebit" role="tab" aria-controls="install-whitebit" aria-selected="false">OKX</a> <a class="nav-link" id="install-whitebit-tab" data-toggle="tab" href="#install-whitebit" role="tab" aria-controls="install-whitebit" aria-selected="false">WhiteBit</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="install-xt-tab" data-toggle="tab" href="#install-xt" role="tab" aria-controls="install-xt" aria-selected="false">XT</a>
</li> </li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
@@ -347,6 +364,9 @@
<div class="tab-pane fade" id="install-whitebit" role="tabpanel" aria-labelledby="install-whitebit-tab"> <div class="tab-pane fade" id="install-whitebit" role="tabpanel" aria-labelledby="install-whitebit-tab">
<pre><code>dotnet add package WhiteBit.Net</code></pre> <pre><code>dotnet add package WhiteBit.Net</code></pre>
</div> </div>
<div class="tab-pane fade" id="install-xt" role="tabpanel" aria-labelledby="install-xt-tab">
<pre><code>dotnet add package XT.Net</code></pre>
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -415,6 +435,9 @@
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="di-whitebit-tab" data-toggle="tab" href="#di-whitebit" role="tab" aria-controls="di-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="di-whitebit-tab" data-toggle="tab" href="#di-whitebit" role="tab" aria-controls="di-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="di-xt-tab" data-toggle="tab" href="#di-xt" role="tab" aria-controls="di-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="di-cc" role="tabpanel" aria-labelledby="di-cc-tab"> <div class="tab-pane fade show active" id="di-cc" role="tabpanel" aria-labelledby="di-cc-tab">
@@ -471,6 +494,9 @@
<div class="tab-pane fade" id="di-whitebit" role="tabpanel" aria-labelledby="di-whitebit-tab"> <div class="tab-pane fade" id="di-whitebit" role="tabpanel" aria-labelledby="di-whitebit-tab">
<pre><code>builder.Services.AddWhiteBit();</code></pre> <pre><code>builder.Services.AddWhiteBit();</code></pre>
</div> </div>
<div class="tab-pane fade" id="di-xt" role="tabpanel" aria-labelledby="di-xt-tab">
<pre><code>builder.Services.AddXT();</code></pre>
</div>
</div> </div>
</div> </div>
@@ -531,6 +557,9 @@
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="interfaces-whitebit-tab" data-toggle="tab" href="#interfaces-whitebit" role="tab" aria-controls="interfaces-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="interfaces-whitebit-tab" data-toggle="tab" href="#interfaces-whitebit" role="tab" aria-controls="interfaces-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="interfaces-xt-tab" data-toggle="tab" href="#interfaces-xt" role="tab" aria-controls="interfaces-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="interfaces-cc" role="tabpanel" aria-labelledby="interfaces-cc-tab"> <div class="tab-pane fade show active" id="interfaces-cc" role="tabpanel" aria-labelledby="interfaces-cc-tab">
@@ -1111,6 +1140,39 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="interfaces-xt" role="tabpanel" aria-labelledby="interfaces-xt-tab">
<table class="table table-bordered">
<tr><th>Interface</th><th>Description</th></tr>
<tr>
<td><code>IXTRestClient</code></td>
<td>The client for accessing the XT REST API</td>
</tr>
<tr>
<td><code>IXTSocketClient</code></td>
<td>The client for accessing the XT Websocket API</td>
</tr>
<tr>
<td><code>IXTOrderBookFactory</code></td>
<td>A factory for creating SymbolOrderBook instances for the XT API</td>
</tr>
<tr>
<td><code>IXTTrackerFactory</code></td>
<td>A factory for creating kline and trade Tracker instances for the XT API</td>
</tr>
<tr>
<td><code>ICryptoRestClient</code></td>
<td>An aggregating client from which multiple different library REST clients can be accessed</td>
</tr>
<tr>
<td><code>ICryptoSocketClient</code></td>
<td>An aggregating client from which multiple different library Websocket clients can be accessed</td>
</tr>
<tr>
<td><code>ISharedClient</code></td>
<td>Various interfaces deriving from ISharedClient which can be used for common functionality</td>
</tr>
</table>
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -1122,7 +1184,7 @@
<p>All clients work with the same principles:</p> <p>All clients work with the same principles:</p>
<ul> <ul>
<li>Mandatory parameters are non-nullable while optional parameters are nullable and will have a default value of null.</li> <li>Mandatory parameters are non-nullable while optional parameters are nullable and will have a default value of null.</li>
<li>Any operation will return a form of <code>CallResult</code>. This result can and should be checked for success using the `Success` property. If `Success` is false the `Error` property will have more info.</li> <li>Any operation will return a form of <code>CallResult</code>. This result can and should be checked for success using the <code>Success</code> property. If <code>Success</code> is false the <code>Error</code> property will have more info.</li>
<li>Clients will not throw exceptions.</li> <li>Clients will not throw exceptions.</li>
</ul> </ul>
</section> </section>
@@ -1190,6 +1252,9 @@
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="rest-whitebit-tab" data-toggle="tab" href="#rest-whitebit" role="tab" aria-controls="rest-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="rest-whitebit-tab" data-toggle="tab" href="#rest-whitebit" role="tab" aria-controls="rest-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="rest-xt-tab" data-toggle="tab" href="#rest-xt" role="tab" aria-controls="rest-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="rest-cc" role="tabpanel" aria-labelledby="rest-cc-tab"> <div class="tab-pane fade show active" id="rest-cc" role="tabpanel" aria-labelledby="rest-cc-tab">
@@ -1404,6 +1469,18 @@ if (!tickersResult.Success)
// Handle error, tickersResult.Error contains more information // Handle error, tickersResult.Error contains more information
} }
else else
{
// Handle data, tickersResult.Data will contain the actual data
}</code></pre>
</div>
<div class="tab-pane fade" id="rest-xt" role="tabpanel" aria-labelledby="rest-xt-tab">
<pre><code>var client = new XTRestClient();
var tickersResult = await client.SpotApi.ExchangeData.GetTickersAsync();
if (!tickersResult.Success)
{
// Handle error, tickersResult.Error contains more information
}
else
{ {
// Handle data, tickersResult.Data will contain the actual data // Handle data, tickersResult.Data will contain the actual data
}</code></pre> }</code></pre>
@@ -1533,6 +1610,9 @@ else
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="socket-whitebit-tab" data-toggle="tab" href="#socket-whitebit" role="tab" aria-controls="socket-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="socket-whitebit-tab" data-toggle="tab" href="#socket-whitebit" role="tab" aria-controls="socket-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="socket-xt-tab" data-toggle="tab" href="#socket-xt" role="tab" aria-controls="socket-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="socket-cc" role="tabpanel" aria-labelledby="socket-cc-tab"> <div class="tab-pane fade show active" id="socket-cc" role="tabpanel" aria-labelledby="socket-cc-tab">
@@ -1711,7 +1791,7 @@ if (!subscribeResult.Success)
} }
// Subscribing was successfull, the data will now be streamed into the data handler</code></pre> // Subscribing was successfull, the data will now be streamed into the data handler</code></pre>
</div> </div>
<div class="tab-pane fade" id="socket-okx" role="tabpanel" aria-labelledby="socket-okx-tab"> <div class="tab-pane fade" id="socket-whitebit" role="tabpanel" aria-labelledby="socket-whitebit-tab">
<pre><code>var client = new WhiteBitSocketClient(); <pre><code>var client = new WhiteBitSocketClient();
var subscribeResult = await client.V4Api.ExchangeData.SubscribeToTickerUpdatesAsync("ETH_USDT", update => { var subscribeResult = await client.V4Api.ExchangeData.SubscribeToTickerUpdatesAsync("ETH_USDT", update => {
// Handle the data update, update.Data will contain the actual data // Handle the data update, update.Data will contain the actual data
@@ -1722,6 +1802,18 @@ if (!subscribeResult.Success)
} }
// Subscribing was successfull, the data will now be streamed into the data handler</code></pre> // Subscribing was successfull, the data will now be streamed into the data handler</code></pre>
</div> </div>
<div class="tab-pane fade" id="socket-xt" role="tabpanel" aria-labelledby="socket-xt-tab">
<pre><code>var client = new XTSocketClient();
var subscribeResult = await client.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("eth_usdt", update => {
// Handle the data update, update.Data will contain the actual data
});
if (!subscribeResult.Success)
{
// Handle error, subscribeResult.Error contains more information on why the subscription failed
}
// Subscribing was successfull, the data will now be streamed into the data handler</code></pre>
</div>
</div> </div>
</div> </div>
@@ -1922,6 +2014,9 @@ var binanceTriggered = CheckForTrigger(lastBinanceTicker);</code></pre>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="shared-whitebit-tab" data-toggle="tab" href="#shared-whitebit" role="tab" aria-controls="shared-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="shared-whitebit-tab" data-toggle="tab" href="#shared-whitebit" role="tab" aria-controls="shared-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="shared-xt-tab" data-toggle="tab" href="#shared-xt" role="tab" aria-controls="shared-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="shared-binance" role="tabpanel" aria-labelledby="shared-binance-tab"> <div class="tab-pane fade show active" id="shared-binance" role="tabpanel" aria-labelledby="shared-binance-tab">
@@ -2105,6 +2200,19 @@ var spotSharedRestClients = whitebitRestClient.V4Api.SharedClient;
// Futures and Spot API common functionality socket client // Futures and Spot API common functionality socket client
var spotSharedSocketClient = whitebitSocketClient.V4Api.SharedClient;</code></pre> var spotSharedSocketClient = whitebitSocketClient.V4Api.SharedClient;</code></pre>
</div> </div>
<div class="tab-pane fade" id="shared-xt" role="tabpanel" aria-labelledby="shared-xt-tab">
<pre><code>// Spot API common functionality rest client
var spotSharedRestClients = xtRestClient.SpotApi.SharedClient;
// Futures API common functionality rest client
var futuresSharedRestClients = xtRestClient.UsdtFuturesApi.SharedClient;
// Spot API common functionality socket client
var spotSharedSocketClient = xtSocketClient.SpotApi.SharedClient;
// Futures API common functionality socket client
var futuresSharedSocketClient = xtSocketClient.FuturesApi.SharedClient;</code></pre>
</div>
</div> </div>
<h4 id="shared_tradingmode">TradingMode</h4> <h4 id="shared_tradingmode">TradingMode</h4>
@@ -2232,6 +2340,7 @@ var balances = await restClient.HTX.SpotApi.SharedClient.GetBalancesAsync(new Ge
<tr><td><code>ILeverageRestClient</code></td><td>For managing leverage for a Futures symbol</td></tr> <tr><td><code>ILeverageRestClient</code></td><td>For managing leverage for a Futures symbol</td></tr>
<tr><td><code>IPositionHistoryRestClient</code></td><td>For requesting the user position closing history</td></tr> <tr><td><code>IPositionHistoryRestClient</code></td><td>For requesting the user position closing history</td></tr>
<tr><td><code>IPositionModeRestClient</code></td><td>For managing the position mode for the user</td></tr> <tr><td><code>IPositionModeRestClient</code></td><td>For managing the position mode for the user</td></tr>
<tr><td><code>IFeeRestClient</code></td><td>For requesting maker and taker trading fee percentages for the user</td></tr>
</table> </table>
<p style="font-style: italic;">Available Socket shared interfaces</p> <p style="font-style: italic;">Available Socket shared interfaces</p>
@@ -2391,6 +2500,9 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="options-whitebit-tab" data-toggle="tab" href="#options-whitebit" role="tab" aria-controls="options-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="options-whitebit-tab" data-toggle="tab" href="#options-whitebit" role="tab" aria-controls="options-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="options-xt-tab" data-toggle="tab" href="#options-xt" role="tab" aria-controls="options-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="options-cc" role="tabpanel" aria-labelledby="options-cc-tab"> <div class="tab-pane fade show active" id="options-cc" role="tabpanel" aria-labelledby="options-cc-tab">
@@ -2605,6 +2717,18 @@ builder.Services.AddOKX(builder.Configuration.GetSection("OKX"));</code></pre>
// see https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples/example-config.json for an example configuration // see https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples/example-config.json for an example configuration
builder.Services.AddWhiteBit(builder.Configuration.GetSection("WhiteBit"));</code></pre> builder.Services.AddWhiteBit(builder.Configuration.GetSection("WhiteBit"));</code></pre>
</div> </div>
<div class="tab-pane fade" id="options-xt" role="tabpanel" aria-labelledby="options-xt-tab">
<pre><code>builder.Services.AddXT(
options => {
options.Rest.RequestTimeout = TimeSpan.FromSeconds(30);
options.Socket.RequestTimeout = TimeSpan.FromSeconds(5);
});
// OR
// see https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples/example-config.json for an example configuration
builder.Services.AddXT(builder.Configuration.GetSection("XT"));</code></pre>
</div>
</div> </div>
</div> </div>
@@ -2666,6 +2790,9 @@ builder.Services.AddWhiteBit(builder.Configuration.GetSection("WhiteBit"));</cod
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="options-whitebit-tab" data-toggle="tab" href="#options-constr-whitebit" role="tab" aria-controls="options-constr-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="options-whitebit-tab" data-toggle="tab" href="#options-constr-whitebit" role="tab" aria-controls="options-constr-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="options-xt-tab" data-toggle="tab" href="#options-constr-xt" role="tab" aria-controls="options-constr-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="options-constr-cc" role="tabpanel" aria-labelledby="options-cc-tab"> <div class="tab-pane fade show active" id="options-constr-cc" role="tabpanel" aria-labelledby="options-cc-tab">
@@ -2772,6 +2899,12 @@ builder.Services.AddWhiteBit(builder.Configuration.GetSection("WhiteBit"));</cod
</div> </div>
<div class="tab-pane fade" id="options-constr-whitebit" role="tabpanel" aria-labelledby="options-whitebit-tab"> <div class="tab-pane fade" id="options-constr-whitebit" role="tabpanel" aria-labelledby="options-whitebit-tab">
<pre><code>var client = new WhiteBitRestClient(opts => <pre><code>var client = new WhiteBitRestClient(opts =>
{
opts.RequestTimeout = TimeSpan.FromSeconds(30);
});</code></pre>
</div>
<div class="tab-pane fade" id="options-constr-xt" role="tabpanel" aria-labelledby="options-xt-tab">
<pre><code>var client = new XTRestClient(opts =>
{ {
opts.RequestTimeout = TimeSpan.FromSeconds(30); opts.RequestTimeout = TimeSpan.FromSeconds(30);
});</code></pre> });</code></pre>
@@ -2834,6 +2967,9 @@ builder.Services.AddWhiteBit(builder.Configuration.GetSection("WhiteBit"));</cod
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="options-whitebit-tab" data-toggle="tab" href="#options-default-whitebit" role="tab" aria-controls="options-default-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="options-whitebit-tab" data-toggle="tab" href="#options-default-whitebit" role="tab" aria-controls="options-default-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="options-xt-tab" data-toggle="tab" href="#options-default-xt" role="tab" aria-controls="options-default-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="options-default-binance" role="tabpanel" aria-labelledby="options-binance-tab"> <div class="tab-pane fade show active" id="options-default-binance" role="tabpanel" aria-labelledby="options-binance-tab">
@@ -2955,6 +3091,13 @@ var client = new OKXRestClient();</code></pre>
}); });
var client = new WhiteBitRestClient();</code></pre> var client = new WhiteBitRestClient();</code></pre>
</div> </div>
<div class="tab-pane fade" id="options-default-xt" role="tabpanel" aria-labelledby="options-xt-tab">
<pre><code>XTRestClient.SetDefaultOptions(options =>
{
options.RequestTimeout = TimeSpan.FromSeconds(30);
});
var client = new XTRestClient();</code></pre>
</div>
</div> </div>
</div> </div>
@@ -3189,6 +3332,9 @@ var client = new WhiteBitRestClient();</code></pre>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="book-whitebit-tab" data-toggle="tab" href="#book-whitebit" role="tab" aria-controls="book-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="book-whitebit-tab" data-toggle="tab" href="#book-whitebit" role="tab" aria-controls="book-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="book-xt-tab" data-toggle="tab" href="#book-xt" role="tab" aria-controls="book-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="book-cryptoclients" role="tabpanel" aria-labelledby="book-cryptoclients-tab"> <div class="tab-pane fade show active" id="book-cryptoclients" role="tabpanel" aria-labelledby="book-cryptoclients-tab">
@@ -3409,6 +3555,19 @@ if (!startResult.Success)
} }
// Book has successfully started and synchronized // Book has successfully started and synchronized
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
await book.StopAsync();
</code></pre>
</div>
<div class="tab-pane fade" id="book-xt" role="tabpanel" aria-labelledby="book-xt-tab">
<pre><code>var book = new XTSymbolOrderBook("eth_usdt");
var startResult = await book.StartAsync();
if (!startResult.Success)
{
// Handle error, error info available in startResult.Error
}
// Book has successfully started and synchronized
// Once no longer needed you can stop the live sync functionality by calling StopAsync() // Once no longer needed you can stop the live sync functionality by calling StopAsync()
await book.StopAsync(); await book.StopAsync();
</code></pre> </code></pre>
@@ -3582,6 +3741,9 @@ foreach (var book in books.Where(b => b.Status == OrderBookStatus.Synced))
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="tracker-whitebit-tab" data-toggle="tab" href="#tracker-whitebit" role="tab" aria-controls="tracker-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="tracker-whitebit-tab" data-toggle="tab" href="#tracker-whitebit" role="tab" aria-controls="tracker-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="tracker-xt-tab" data-toggle="tab" href="#tracker-xt" role="tab" aria-controls="tracker-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="tracker-cryptoclients" role="tabpanel" aria-labelledby="tracker-cryptoclients-tab"> <div class="tab-pane fade show active" id="tracker-cryptoclients" role="tabpanel" aria-labelledby="tracker-cryptoclients-tab">
@@ -3921,6 +4083,26 @@ if (!startResult.Success)
// Tracker has successfully started // Tracker has successfully started
// Note that it might not be fully synced yet, check tracker.Status for this. // Note that it might not be fully synced yet, check tracker.Status for this.
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
await tracker.StopAsync();
</code></pre>
</div>
<div class="tab-pane fade" id="tracker-xt" role="tabpanel" aria-labelledby="tracker-xt-tab">
<pre><code>// Either create a new factory or inject the IXTTrackerFactory interface
var factory = new XTTrackerFactory();
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
var startResult = await tracker.StartAsync();
if (!startResult.Success)
{
// Handle error, error info available in startResult.Error
}
// Tracker has successfully started
// Note that it might not be fully synced yet, check tracker.Status for this.
// Once no longer needed you can stop the live sync functionality by calling StopAsync() // Once no longer needed you can stop the live sync functionality by calling StopAsync()
await tracker.StopAsync(); await tracker.StopAsync();
</code></pre> </code></pre>
@@ -4274,6 +4456,9 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="limit-whitebit-tab" data-toggle="tab" href="#limit-whitebit" role="tab" aria-controls="limit-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="limit-whitebit-tab" data-toggle="tab" href="#limit-whitebit" role="tab" aria-controls="limit-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="limit-xt-tab" data-toggle="tab" href="#limit-xt" role="tab" aria-controls="limit-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="limit-cc" role="tabpanel" aria-labelledby="limit-cc-tab"> <div class="tab-pane fade show active" id="limit-cc" role="tabpanel" aria-labelledby="limit-cc-tab">
@@ -4474,6 +4659,20 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
<p>To be notified of when a rate limit is hit the static <code>WhiteBitExchange.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p> <p>To be notified of when a rate limit is hit the static <code>WhiteBitExchange.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p>
<pre><code>WhiteBitExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent); <pre><code>WhiteBitExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent);
</code></pre>
</div>
<div class="tab-pane fade" id="limit-xt" role="tabpanel" aria-labelledby="limit-xt-tab">
<pre><code>services.AddXT(x =>
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
}, x =>
{
x.RatelimiterEnabled = true;
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
});</code></pre>
<p>To be notified of when a rate limit is hit the static <code>XTExchange.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p>
<pre><code>XTExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent);
</code></pre> </code></pre>
</div> </div>
</div> </div>
@@ -4576,6 +4775,9 @@ var responseSource = result.DataSource;</code></pre>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="example-symbols-whitebit-tab" data-toggle="tab" href="#example-symbols-whitebit" role="tab" aria-controls="example-symbols-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="example-symbols-whitebit-tab" data-toggle="tab" href="#example-symbols-whitebit" role="tab" aria-controls="example-symbols-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="example-symbols-xt-tab" data-toggle="tab" href="#example-symbols-xt" role="tab" aria-controls="example-symbols-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="example-symbols-general" role="tabpanel" aria-labelledby="example-symbols-general-tab"> <div class="tab-pane fade show active" id="example-symbols-general" role="tabpanel" aria-labelledby="example-symbols-general-tab">
@@ -4637,6 +4839,9 @@ await exchangeRestClient.GetSpotSymbolsAsync(new GetSymbolsRequest(), ["Binance"
<div class="tab-pane fade" id="example-symbols-whitebit" role="tabpanel" aria-labelledby="example-symbols-whitebit-tab"> <div class="tab-pane fade" id="example-symbols-whitebit" role="tabpanel" aria-labelledby="example-symbols-whitebit-tab">
<pre><code>await whitebitClient.V4Api.ExchangeData.GetSymbolsAsync();</code></pre> <pre><code>await whitebitClient.V4Api.ExchangeData.GetSymbolsAsync();</code></pre>
</div> </div>
<div class="tab-pane fade" id="example-symbols-xt" role="tabpanel" aria-labelledby="example-symbols-xt-tab">
<pre><code>await xtClient.SpotApi.ExchangeData.GetSymbolsAsync();</code></pre>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -4706,6 +4911,9 @@ await exchangeRestClient.GetSpotSymbolsAsync(new GetSymbolsRequest(), ["Binance"
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="example-ticker-whitebit-tab" data-toggle="tab" href="#example-ticker-whitebit" role="tab" aria-controls="example-ticker-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="example-ticker-whitebit-tab" data-toggle="tab" href="#example-ticker-whitebit" role="tab" aria-controls="example-ticker-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="example-ticker-xt-tab" data-toggle="tab" href="#example-ticker-xt" role="tab" aria-controls="example-ticker-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="example-ticker-general" role="tabpanel" aria-labelledby="example-ticker-general-tab"> <div class="tab-pane fade show active" id="example-ticker-general" role="tabpanel" aria-labelledby="example-ticker-general-tab">
@@ -4770,6 +4978,9 @@ await coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");</
<pre><code>// WhiteBit API doesn't offer a symbol filter, so we have to filter client side <pre><code>// WhiteBit API doesn't offer a symbol filter, so we have to filter client side
var tickersResult = await whitebitClient.V4Api.ExchangeData.GetTickersAsync(); var tickersResult = await whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var ticker = tickersResult.Data.Single(x => x.Symbol == "BTC_USDT");</code></pre> var ticker = tickersResult.Data.Single(x => x.Symbol == "BTC_USDT");</code></pre>
</div>
<div class="tab-pane fade" id="example-ticker-xt" role="tabpanel" aria-labelledby="example-ticker-xt-tab">
<pre><code>await xtClient.SpotApi.ExchangeData.GetTickersAsync("btc-usdt");</code></pre>
</div> </div>
</div> </div>
</div> </div>
@@ -4840,6 +5051,9 @@ var ticker = tickersResult.Data.Single(x => x.Symbol == "BTC_USDT");</code></pre
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="example-balances-whitebit-tab" data-toggle="tab" href="#example-balances-whitebit" role="tab" aria-controls="example-balances-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="example-balances-whitebit-tab" data-toggle="tab" href="#example-balances-whitebit" role="tab" aria-controls="example-balances-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="example-balances-xt-tab" data-toggle="tab" href="#example-balances-xt" role="tab" aria-controls="example-balances-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="example-balances-general" role="tabpanel" aria-labelledby="example-balances-general-tab"> <div class="tab-pane fade show active" id="example-balances-general" role="tabpanel" aria-labelledby="example-balances-general-tab">
@@ -4905,6 +5119,9 @@ var result = await htxClient.SpotApi.Account.GetBalancesAsync();</code></pre>
<div class="tab-pane fade" id="example-balances-whitebit" role="tabpanel" aria-labelledby="example-balances-whitebit-tab"> <div class="tab-pane fade" id="example-balances-whitebit" role="tabpanel" aria-labelledby="example-balances-whitebit-tab">
<pre><code>await whitebitClient.V4Api.Account.GetSpotBalancesAsync();</code></pre> <pre><code>await whitebitClient.V4Api.Account.GetSpotBalancesAsync();</code></pre>
</div> </div>
<div class="tab-pane fade" id="example-balances-xt" role="tabpanel" aria-labelledby="example-balances-xt-tab">
<pre><code>await xtClient.SpotApi.Account.GetBalancesAsync();</code></pre>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -4972,7 +5189,10 @@ var result = await htxClient.SpotApi.Account.GetBalancesAsync();</code></pre>
<a class="nav-link" id="example-place-okx-tab" data-toggle="tab" href="#example-place-okx" role="tab" aria-controls="example-place-okx" aria-selected="false">OKX</a> <a class="nav-link" id="example-place-okx-tab" data-toggle="tab" href="#example-place-okx" role="tab" aria-controls="example-place-okx" aria-selected="false">OKX</a>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="example-place-okx-tab" data-toggle="tab" href="#example-place-okx" role="tab" aria-controls="example-place-okx" aria-selected="false">WhiteBit</a> <a class="nav-link" id="example-place-whitebit-tab" data-toggle="tab" href="#example-place-whitebit" role="tab" aria-controls="example-place-whitebit" aria-selected="false">WhiteBit</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="example-place-xt-tab" data-toggle="tab" href="#example-place-xt" role="tab" aria-controls="example-place-xt" aria-selected="false">XT</a>
</li> </li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
@@ -5037,6 +5257,9 @@ var result = await htxClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCUSD
<div class="tab-pane fade" id="example-place-whitebit" role="tabpanel" aria-labelledby="example-place-whitebit-tab"> <div class="tab-pane fade" id="example-place-whitebit" role="tabpanel" aria-labelledby="example-place-whitebit-tab">
<pre><code>await whitebitClient.V4Api.Trading.PlaceSpotOrderAsync("BTC_USDT", OrderSide.Buy, NewOrderType.Limit, 0.1m, price: 50000);</code></pre> <pre><code>await whitebitClient.V4Api.Trading.PlaceSpotOrderAsync("BTC_USDT", OrderSide.Buy, NewOrderType.Limit, 0.1m, price: 50000);</code></pre>
</div> </div>
<div class="tab-pane fade" id="example-place-xt" role="tabpanel" aria-labelledby="example-place-xt-tab">
<pre><code>await xtClient.SpotApi.Trading.PlaceOrderAsync("eth_usdt", OrderSide.Buy, OrderType.Limit, TimeInForce.GoodTillCanceled, BusinessType.Spot, 0.1m, price: 50000);</code></pre>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -5106,6 +5329,9 @@ var result = await htxClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCUSD
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="example-stream-ticker-whitebit-tab" data-toggle="tab" href="#example-stream-ticker-whitebit" role="tab" aria-controls="example-stream-ticker-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="example-stream-ticker-whitebit-tab" data-toggle="tab" href="#example-stream-ticker-whitebit" role="tab" aria-controls="example-stream-ticker-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="example-stream-ticker-xt-tab" data-toggle="tab" href="#example-stream-ticker-xt" role="tab" aria-controls="example-stream-ticker-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="example-stream-ticker-cc" role="tabpanel" aria-labelledby="example-stream-ticker-cc-tab"> <div class="tab-pane fade show active" id="example-stream-ticker-cc" role="tabpanel" aria-labelledby="example-stream-ticker-cc-tab">
@@ -5206,6 +5432,12 @@ await exchangeSocketClient.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequ
<pre><code>await whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_USDT", data => { <pre><code>await whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_USDT", data => {
// Handle update // Handle update
}); });
</code></pre>
</div>
<div class="tab-pane fade" id="example-stream-ticker-xt" role="tabpanel" aria-labelledby="example-stream-ticker-xt-tab">
<pre><code>await xtSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_usdt", data => {
// Handle update
});
</code></pre> </code></pre>
</div> </div>
</div> </div>
@@ -5277,6 +5509,9 @@ await exchangeSocketClient.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequ
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a class="nav-link" id="example-stream-order-whitebit-tab" data-toggle="tab" href="#example-stream-order-whitebit" role="tab" aria-controls="example-stream-order-whitebit" aria-selected="false">WhiteBit</a> <a class="nav-link" id="example-stream-order-whitebit-tab" data-toggle="tab" href="#example-stream-order-whitebit" role="tab" aria-controls="example-stream-order-whitebit" aria-selected="false">WhiteBit</a>
</li> </li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="example-stream-order-xt-tab" data-toggle="tab" href="#example-stream-order-xt" role="tab" aria-controls="example-stream-order-xt" aria-selected="false">XT</a>
</li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <div class="tab-content my-3" id="myTabContent">
<div class="tab-pane fade show active" id="example-stream-order-cc" role="tabpanel" aria-labelledby="example-stream-order-cc-tab"> <div class="tab-pane fade show active" id="example-stream-order-cc" role="tabpanel" aria-labelledby="example-stream-order-cc-tab">
@@ -5432,6 +5667,26 @@ _ = Task.Run(async () => {
await whitebitSocketClient.V4Api.SubscribeToOpenOrderUpdatesAsync(["ETH_USDT", "BTC_USDT"], data => { await whitebitSocketClient.V4Api.SubscribeToOpenOrderUpdatesAsync(["ETH_USDT", "BTC_USDT"], data => {
// Handle update // Handle update
}); });
</code></pre>
</div>
<div class="tab-pane fade" id="example-stream-order-xt" role="tabpanel" aria-labelledby="example-stream-order-xt-tab">
<pre><code>// Retrieve the token
var listenKey = await xtRestClient.SpotApi.Account.GetWebsocketTokenAsync();
// Subscribe using the key
await xtSocketClient.SpotApi.SubscribeToBalanceUpdatesAsync(listenKey.Data, data => {
// Handle update
});
// The listen key will stay valid for 48 hours, after this no updates will be send anymore
// To extend the life time of the token it is recommended to call the GetWebsocketTokenAsync method at a set interval which will extend the lifetime
_ = Task.Run(async () => {
while (true)
{
await Task.Delay(Timespan.FromHours(4));
await xtRestClient.SpotApi.Account.GetWebsocketTokenAsync();
}
});
</code></pre> </code></pre>
</div> </div>
</div> </div>