mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-14 09:52:53 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ab85d4c26 | |||
| be68115099 | |||
| ff0550b0fb | |||
| 1ab1e008fc | |||
| 6f30c72608 | |||
| e927bc3d20 | |||
| 09ed7d1436 | |||
| 6fed657ea6 | |||
| 1555f8da0c | |||
| 68b28fc875 | |||
| 5d50d8cde8 | |||
| 9ff673d8be | |||
| 3e5a34fb56 | |||
| 64ee50d98c | |||
| 6a105c6f8f | |||
| 287aadc720 | |||
| 7229438a0b | |||
| 444af98a15 | |||
| 70c6fa1bbb | |||
| d27f394b46 | |||
| c8c98e13d0 |
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
result = messageEvent.Data;
|
result = messageEvent.Data;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
@@ -107,7 +106,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Caching
|
||||||
|
{
|
||||||
|
internal class MemoryCache
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="value">Cache value</param>
|
||||||
|
public void Add(string key, object value)
|
||||||
|
{
|
||||||
|
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||||
|
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a cached value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="maxAge">The max age of the cached entry</param>
|
||||||
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
|
public object? Get(string key, TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue(key, out CacheItem value);
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryRemove(key, out _);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CacheItem
|
||||||
|
{
|
||||||
|
public DateTime CacheTime { get; }
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public CacheItem(DateTime cacheTime, object value)
|
||||||
|
{
|
||||||
|
CacheTime = cacheTime;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
@@ -17,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
|
|||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Requests;
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -85,6 +87,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory cache
|
||||||
|
/// </summary>
|
||||||
|
private static MemoryCache _cache = new MemoryCache();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -190,6 +196,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null) where T : class
|
int? weight = null) where T : class
|
||||||
{
|
{
|
||||||
|
var key = baseAddress + definition + uriParameters?.ToFormData();
|
||||||
|
if (ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -215,6 +236,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,6 +472,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
/// <param name="gate">The ratelimit gate to use</param>
|
||||||
|
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNull]
|
[return: NotNull]
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
@@ -458,9 +486,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
IRateLimitGate? gate = null
|
IRateLimitGate? gate = null,
|
||||||
|
bool preventCaching = false
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
|
var key = uri.ToString() + method + signed + parameters?.ToFormData();
|
||||||
|
if (ShouldCache(method) && !preventCaching)
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -478,6 +522,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(method) &&
|
||||||
|
!preventCaching)
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,47 +668,47 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(T) == typeof(object))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Json response received
|
// Json response received
|
||||||
var parsedError = TryParseError(accessor);
|
var parsedError = TryParseError(accessor);
|
||||||
if (parsedError != null)
|
if (parsedError != null)
|
||||||
// Success status code, but TryParseError determined it was an error response
|
// Success status code, but TryParseError determined it was an error response
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
var deserializeResult = accessor.Deserialize<T>();
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException requestException)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
var exceptionInfo = requestException.ToLogString();
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -911,14 +962,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
if (timeSyncParams == null)
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -947,7 +998,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& definition.Method == HttpMethod.Get
|
||||||
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
|
private bool ShouldCache(HttpMethod method)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& method == HttpMethod.Get;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -52,11 +53,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true a subscription will accept message before the confirmation of a subscription has been received
|
|
||||||
/// </summary>
|
|
||||||
protected bool HandleMessageBeforeConfirmation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiters
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -72,6 +68,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of address to keep an alive connection to
|
||||||
|
/// </summary>
|
||||||
|
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -136,6 +137,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep an open connection to this url
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="auth"></param>
|
||||||
|
protected virtual void SetDedicatedConnection(string url, bool auth)
|
||||||
|
{
|
||||||
|
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -198,12 +209,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
|
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
// Add a subscription on the socket connection
|
||||||
var success = socketConnection.AddSubscription(subscription);
|
var success = socketConnection.AddSubscription(subscription);
|
||||||
@@ -250,11 +260,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
if (isTimeout && subscription.Confirmed)
|
||||||
var unsubscribe = subResult.Error is CancellationRequestedError;
|
{
|
||||||
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
// No response received, but the subscription did receive updates. We'll assume success
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||||
|
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||||
|
await socketConnection.CloseAsync(subscription, isTimeout).ConfigureAwait(false);
|
||||||
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
@@ -281,10 +298,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query)
|
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return QueryAsync(BaseAddress, query);
|
return QueryAsync(BaseAddress, query, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -294,18 +312,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query)
|
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<THandlerResponse>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
@@ -334,7 +356,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await socketConnection.SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(query).ConfigureAwait(false);
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
|
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -358,7 +383,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
|
|
||||||
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -372,7 +401,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult(new NoApiCredentialsError());
|
return new CallResult(new NoApiCredentialsError());
|
||||||
|
|
||||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||||
var authRequest = GetAuthenticationRequest();
|
var authRequest = GetAuthenticationRequest(socket);
|
||||||
if (authRequest != null)
|
if (authRequest != null)
|
||||||
{
|
{
|
||||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||||
@@ -397,7 +426,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Should return the request which can be used to authenticate a socket connection
|
/// Should return the request which can be used to authenticate a socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
|
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
/// Adds a system subscription. Used for example to reply to ping requests
|
||||||
@@ -446,19 +475,31 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
|
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
&& s.Value.Connected);
|
||||||
if (result != null)
|
|
||||||
|
SocketConnection connection;
|
||||||
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(result);
|
return new CallResult<SocketConnection>(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
@@ -475,6 +516,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||||
|
|
||||||
foreach (var ptg in PeriodicTaskRegistrations)
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||||
@@ -517,7 +559,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="address">The address to connect to</param>
|
/// <param name="address">The address to connect to</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
@@ -594,8 +636,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
foreach (var sub in socketList)
|
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||||
tasks.Add(sub.CloseAsync());
|
tasks.Add(connection.CloseAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
@@ -618,6 +660,23 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<CallResult> PrepareConnectionsAsync()
|
||||||
|
{
|
||||||
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
|
{
|
||||||
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult(connectResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -701,11 +760,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
_logger.DisposingSocketClient();
|
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||||
_ = UnsubscribeAllAsync();
|
if (socketList.Any())
|
||||||
|
_logger.DisposingSocketClient();
|
||||||
|
|
||||||
|
foreach (var connection in socketList)
|
||||||
|
{
|
||||||
|
tasks.Add(connection.CloseAsync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
throw new Exception("1");
|
throw new Exception("Not an array");
|
||||||
|
|
||||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
||||||
attributes = CacheTypeAttributes(objectType);
|
attributes = CacheTypeAttributes(objectType);
|
||||||
@@ -92,8 +92,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||||
var targetType = attribute.PropertyInfo.PropertyType;
|
if (attribute == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var targetType = attribute.PropertyInfo.PropertyType;
|
||||||
|
|
||||||
object? value = null;
|
object? value = null;
|
||||||
if (attribute.JsonConverterType != null)
|
if (attribute.JsonConverterType != null)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
writer.WriteStringValue("");
|
||||||
|
|
||||||
|
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>7.5.2</PackageVersion>
|
<PackageVersion>7.7.2</PackageVersion>
|
||||||
<AssemblyVersion>7.5.2</AssemblyVersion>
|
<AssemblyVersion>7.7.2</AssemblyVersion>
|
||||||
<FileVersion>7.5.2</FileVersion>
|
<FileVersion>7.7.2</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>
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ namespace CryptoExchange.Net
|
|||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var kvp in parameters)
|
foreach (var kvp in parameters)
|
||||||
{
|
{
|
||||||
|
if (kvp.Value is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
if (kvp.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
var array = (Array)kvp.Value;
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this listener can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The identifiers for this processor
|
/// The identifiers for this processor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<string> ListenerIdentifiers { get; }
|
public HashSet<string> ListenerIdentifiers { get; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepare connections which can subsequently be used for sending websocket requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CallResult> PrepareConnectionsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
||||||
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||||
|
|
||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
@@ -65,6 +68,21 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4008, "RestApiSendRequest"),
|
new EventId(4008, "RestApiSendRequest"),
|
||||||
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||||
|
|
||||||
|
_restApiCheckingCache = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4009, "RestApiCheckingCache"),
|
||||||
|
"Checking cache for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4010, "RestApiCacheHit"),
|
||||||
|
"Cache hit for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4011, "RestApiCacheNotHit"),
|
||||||
|
"Cache not hit for key {Key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
||||||
@@ -111,5 +129,20 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void CheckingCache(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCheckingCache(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheHit(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheNotHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheNotHit(logger, key, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Wait for the AutoResetEvent to be set
|
/// Wait for the AutoResetEvent to be set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task<bool> WaitAsync(TimeSpan? timeout = null)
|
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock (_waits)
|
lock (_waits)
|
||||||
{
|
{
|
||||||
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
if (ct.IsCancellationRequested)
|
||||||
if(timeout != null)
|
return _completed;
|
||||||
{
|
|
||||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
|
||||||
var registration = cancellationSource.Token.Register(() =>
|
|
||||||
{
|
|
||||||
lock (_waits)
|
|
||||||
{
|
|
||||||
tcs.TrySetResult(false);
|
|
||||||
|
|
||||||
// Not the cleanest but it works
|
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
if (timeout.HasValue)
|
||||||
}
|
{
|
||||||
}, useSynchronizationContext: false);
|
var timeoutSource = new CancellationTokenSource(timeout.Value);
|
||||||
|
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
|
||||||
|
ct = cancellationSource.Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var registration = ct.Register(() =>
|
||||||
|
{
|
||||||
|
lock (_waits)
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(false);
|
||||||
|
|
||||||
|
// Not the cleanest but it works
|
||||||
|
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||||
|
}
|
||||||
|
}, useSynchronizationContext: false);
|
||||||
|
|
||||||
|
|
||||||
_waits.Enqueue(tcs);
|
_waits.Enqueue(tcs);
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? ResponseTime { get; set; }
|
public TimeSpan? ResponseTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of this result
|
||||||
|
/// </summary>
|
||||||
|
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new result
|
/// Create a new result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -344,6 +349,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBody"></param>
|
/// <param name="requestBody"></param>
|
||||||
/// <param name="requestMethod"></param>
|
/// <param name="requestMethod"></param>
|
||||||
/// <param name="requestHeaders"></param>
|
/// <param name="requestHeaders"></param>
|
||||||
|
/// <param name="dataSource"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
@@ -357,6 +363,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||||
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
{
|
{
|
||||||
@@ -370,6 +377,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
RequestHeaders = requestHeaders;
|
RequestHeaders = requestHeaders;
|
||||||
RequestMethod = requestMethod;
|
RequestMethod = requestMethod;
|
||||||
|
DataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -393,7 +401,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <param name="error">The error</param>
|
||||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -403,7 +411,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -414,7 +422,16 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
public new WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a copy of this result with data source set to cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
internal WebCallResult<T> Cached()
|
||||||
|
{
|
||||||
|
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -169,4 +169,38 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Snapshot
|
Snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public enum ReconnectPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect is disabled
|
||||||
|
/// </summary>
|
||||||
|
Disabled,
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed delay of `ReconnectInterval` between retries
|
||||||
|
/// </summary>
|
||||||
|
FixedDelay,
|
||||||
|
/// <summary>
|
||||||
|
/// Backof policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5
|
||||||
|
/// </summary>
|
||||||
|
ExponentialBackoff
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of the result
|
||||||
|
/// </summary>
|
||||||
|
public enum ResultDataSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// From server
|
||||||
|
/// </summary>
|
||||||
|
Server,
|
||||||
|
/// <summary>
|
||||||
|
/// From cache
|
||||||
|
/// </summary>
|
||||||
|
Cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option
|
||||||
|
/// </summary>
|
||||||
|
public bool CachingEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a copy of this options
|
/// Create a copy of this options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
Proxy = Proxy,
|
Proxy = Proxy,
|
||||||
RequestTimeout = RequestTimeout,
|
RequestTimeout = RequestTimeout,
|
||||||
RateLimiterEnabled = RateLimiterEnabled,
|
RateLimiterEnabled = RateLimiterEnabled,
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour
|
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||||
|
CachingEnabled = CachingEnabled,
|
||||||
|
CachingMaxAge = CachingMaxAge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
@@ -9,15 +10,15 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
public class SocketExchangeOptions : ExchangeOptions
|
public class SocketExchangeOptions : ExchangeOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not the socket should automatically reconnect when losing connection
|
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time to wait between reconnect attempts
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
AutoReconnect = AutoReconnect,
|
ReconnectPolicy = ReconnectPolicy,
|
||||||
DelayAfterConnect = DelayAfterConnect,
|
DelayAfterConnect = DelayAfterConnect,
|
||||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||||
ReconnectInterval = ReconnectInterval,
|
ReconnectInterval = ReconnectInterval,
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
public TimeSpan? EndpointLimitPeriod { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this request should never be cached
|
||||||
|
/// </summary>
|
||||||
|
public bool PreventCaching { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBodyFormat">Request body format</param>
|
/// <param name="requestBodyFormat">Request body format</param>
|
||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@@ -59,7 +60,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
TimeSpan? endpointLimitPeriod = null,
|
TimeSpan? endpointLimitPeriod = null,
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null)
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
bool? preventCaching = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(method + path, out var def))
|
if (!_definitions.TryGetValue(method + path, out var def))
|
||||||
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
|
PreventCaching = preventCaching ?? false
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(method + path, def);
|
_definitions.TryAdd(method + path, def);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,20 +26,20 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time to wait between reconnect attempts
|
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy for the connection
|
/// Proxy for the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiProxy? Proxy { get; set; }
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the socket should automatically reconnect when connection is lost
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">Uri</param>
|
/// <param name="uri">Uri</param>
|
||||||
/// <param name="autoReconnect">Auto reconnect</param>
|
/// <param name="policy">Reconnect policy</param>
|
||||||
public WebSocketParameters(Uri uri, bool autoReconnect)
|
public WebSocketParameters(Uri uri, ReconnectPolicy policy)
|
||||||
{
|
{
|
||||||
Uri = uri;
|
Uri = uri;
|
||||||
AutoReconnect = autoReconnect;
|
ReconnectPolicy = policy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||||
|
|
||||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||||
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
|
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||||
{
|
{
|
||||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||||
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime()
|
private TimeSpan DetermineWaitTime()
|
||||||
{
|
{
|
||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||||
return wait.Add(_fixedWindowBuffer);
|
var result = wait.Add(_fixedWindowBuffer);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private readonly List<LimitEntry> _entries;
|
private readonly List<LimitEntry> _entries;
|
||||||
private int _currentWeight = 0;
|
private int _currentWeight = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional wait time to apply to account for fluctuating request times
|
||||||
|
/// </summary>
|
||||||
|
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||||
{
|
{
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
removedWeight += entry.Weight;
|
removedWeight += entry.Weight;
|
||||||
if (removedWeight >= weightToRemove)
|
if (removedWeight >= weightToRemove)
|
||||||
{
|
{
|
||||||
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
|
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
private ProcessState _processState;
|
private ProcessState _processState;
|
||||||
private DateTime _lastReconnectTime;
|
private DateTime _lastReconnectTime;
|
||||||
private string _baseAddress;
|
private string _baseAddress;
|
||||||
|
private int _reconnectAttempt;
|
||||||
|
|
||||||
private const int _receiveBufferSize = 1048576;
|
private const int _receiveBufferSize = 1048576;
|
||||||
private const int _sendBufferSize = 4096;
|
private const int _sendBufferSize = 4096;
|
||||||
@@ -246,12 +247,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await _closeTask.ConfigureAwait(false);
|
await _closeTask.ConfigureAwait(false);
|
||||||
_closeTask = null;
|
_closeTask = null;
|
||||||
|
|
||||||
if (!Parameters.AutoReconnect)
|
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
|
||||||
{
|
{
|
||||||
_processState = ProcessState.Idle;
|
_processState = ProcessState.Idle;
|
||||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_stopRequested)
|
if (!_stopRequested)
|
||||||
{
|
{
|
||||||
@@ -259,9 +260,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
// Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected
|
||||||
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
var initialDelay = GetReconnectDelay();
|
||||||
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
await Task.Delay(initialDelay).ConfigureAwait(false);
|
||||||
|
|
||||||
while (!_stopRequested)
|
while (!_stopRequested)
|
||||||
{
|
{
|
||||||
@@ -282,13 +283,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||||
|
|
||||||
|
_reconnectAttempt++;
|
||||||
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||||
if (!connected)
|
if (!connected)
|
||||||
{
|
{
|
||||||
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
// Delay between reconnect attempts
|
||||||
|
var delay = GetReconnectDelay();
|
||||||
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_reconnectAttempt = 0;
|
||||||
_lastReconnectTime = DateTime.UtcNow;
|
_lastReconnectTime = DateTime.UtcNow;
|
||||||
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
@@ -298,6 +303,24 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_processState = ProcessState.Idle;
|
_processState = ProcessState.Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TimeSpan GetReconnectDelay()
|
||||||
|
{
|
||||||
|
if (_reconnectAttempt == 0)
|
||||||
|
{
|
||||||
|
// Means this is directly after disconnecting. Only delay if the last reconnect time is very recent
|
||||||
|
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||||
|
if (sinceLastReconnect < TimeSpan.FromSeconds(5))
|
||||||
|
return TimeSpan.FromSeconds(5) - sinceLastReconnect;
|
||||||
|
|
||||||
|
return TimeSpan.FromMilliseconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var delay = Parameters.ReconnectPolicy == ReconnectPolicy.FixedDelay ? Parameters.ReconnectInterval : TimeSpan.FromSeconds(Math.Pow(2, Math.Min(5, _reconnectAttempt)));
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
return delay;
|
||||||
|
return TimeSpan.FromMilliseconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(int id, string data, int weight)
|
public virtual void Send(int id, string data, int weight)
|
||||||
{
|
{
|
||||||
@@ -439,11 +462,19 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
if (Parameters.RateLimiter != null)
|
if (Parameters.RateLimiter != null)
|
||||||
{
|
{
|
||||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
try
|
||||||
if (!limitResult)
|
|
||||||
{
|
{
|
||||||
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
||||||
continue;
|
if (!limitResult)
|
||||||
|
{
|
||||||
|
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// canceled
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dedicated connection configuration
|
||||||
|
/// </summary>
|
||||||
|
public class DedicatedConnectionConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket address
|
||||||
|
/// </summary>
|
||||||
|
public string SocketAddress { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// authenticated
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; } = ExchangeHelpers.NextId();
|
public int Id { get; } = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData => true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Has this query been completed
|
/// Has this query been completed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -116,8 +111,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Wait untill timeout or the request is competed
|
/// Wait untill timeout or the request is competed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="timeout"></param>
|
/// <param name="timeout"></param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false);
|
public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this connection should be kept alive even when there is no subscription
|
||||||
|
/// </summary>
|
||||||
|
public bool DedicatedRequestConnection { get; internal set; }
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
private readonly object _listenersLock;
|
private readonly object _listenersLock;
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
@@ -443,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// 4. Get the listeners interested in this message
|
// 4. Get the listeners interested in this message
|
||||||
List<IMessageProcessor> processors;
|
List<IMessageProcessor> processors;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList();
|
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
|
||||||
|
|
||||||
if (processors.Count == 0)
|
if (processors.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -451,7 +456,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
List<string> listenerIds;
|
List<string> listenerIds;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
listenerIds = _listeners.Where(l => l.CanHandleData).SelectMany(l => l.ListenerIdentifiers).ToList();
|
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
||||||
UnhandledMessage?.Invoke(_accessor);
|
UnhandledMessage?.Invoke(_accessor);
|
||||||
}
|
}
|
||||||
@@ -478,6 +483,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
|
||||||
|
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||||
|
subscriptionProcessor.Confirmed = true;
|
||||||
|
|
||||||
// 6. Deserialize the message
|
// 6. Deserialize the message
|
||||||
object? deserialized = null;
|
object? deserialized = null;
|
||||||
desCache?.TryGetValue(messageType, out deserialized);
|
desCache?.TryGetValue(messageType, out deserialized);
|
||||||
@@ -604,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
{
|
{
|
||||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed);
|
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||||
if (shouldCloseConnection)
|
if (shouldCloseConnection)
|
||||||
Status = SocketStatus.Closing;
|
Status = SocketStatus.Closing;
|
||||||
}
|
}
|
||||||
@@ -686,10 +695,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">Query to send</param>
|
/// <param name="query">Query to send</param>
|
||||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null)
|
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
|
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||||
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,14 +710,15 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="query">Query to send</param>
|
/// <param name="query">Query to send</param>
|
||||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, ManualResetEvent? continueEvent = null)
|
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
|
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||||
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
|
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent)
|
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock(_listenersLock)
|
lock(_listenersLock)
|
||||||
_listeners.Add(query);
|
_listeners.Add(query);
|
||||||
@@ -724,7 +735,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
{
|
{
|
||||||
@@ -735,11 +746,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (query.Completed)
|
if (query.Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await query.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
|
await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
if (query.Completed)
|
if (query.Completed)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
query.Fail(new CancellationRequestedError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -799,20 +816,23 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
bool anySubscriptions;
|
if (!DedicatedRequestConnection)
|
||||||
lock (_listenersLock)
|
|
||||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
|
||||||
if (!anySubscriptions)
|
|
||||||
{
|
{
|
||||||
// No need to resubscribe anything
|
bool anySubscriptions;
|
||||||
_logger.NothingToResubscribeCloseConnection(SocketId);
|
lock (_listenersLock)
|
||||||
_ = _socket.CloseAsync();
|
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||||
return new CallResult(null);
|
if (!anySubscriptions)
|
||||||
|
{
|
||||||
|
// No need to resubscribe anything
|
||||||
|
_logger.NothingToResubscribeCloseConnection(SocketId);
|
||||||
|
_ = _socket.CloseAsync();
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool anyAuthenticated;
|
bool anyAuthenticated;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated);
|
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
|
||||||
if (anyAuthenticated)
|
if (anyAuthenticated)
|
||||||
{
|
{
|
||||||
// If we reconnected a authenticated connection we need to re-authenticate
|
// If we reconnected a authenticated connection we need to re-authenticate
|
||||||
@@ -861,6 +881,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
waitEvent.Set();
|
waitEvent.Set();
|
||||||
|
if (r.Result.Success)
|
||||||
|
subscription.Confirmed = true;
|
||||||
return r.Result;
|
return r.Result;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -870,9 +892,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return taskList.First(t => !t.Result.Success).Result;
|
return taskList.First(t => !t.Result.Success).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subscription in subList)
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
|
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total amount of invocations
|
/// Total amount of invocations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,11 +37,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Has the subscription been confirmed
|
/// Has the subscription been confirmed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Confirmed { get; set; }
|
public bool Confirmed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this subscription should handle update messages before confirmation
|
|
||||||
/// </summary>
|
|
||||||
public bool HandleUpdatesBeforeConfirmation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the subscription closed
|
/// Is the subscription closed
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
else if (jObj.Type == JTokenType.Array)
|
else if (jObj.Type == JTokenType.Array)
|
||||||
{
|
{
|
||||||
var resultObj = enumerator.Current;
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
else if (jObj.Type == JTokenType.Array)
|
else if (jObj.Type == JTokenType.Array)
|
||||||
{
|
{
|
||||||
var resultObj = enumerator.Current;
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
|||||||
@@ -30,9 +30,14 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public bool IsClosed => !Connected;
|
public bool IsClosed => !Connected;
|
||||||
public bool IsOpen => Connected;
|
public bool IsOpen => Connected;
|
||||||
public double IncomingKbps => 0;
|
public double IncomingKbps => 0;
|
||||||
public Uri Uri => new("wss://test.com/ws");
|
public Uri Uri { get; set; }
|
||||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
|
public TestSocket(string address)
|
||||||
|
{
|
||||||
|
Uri = new Uri(address);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<CallResult> ConnectAsync()
|
public Task<CallResult> ConnectAsync()
|
||||||
{
|
{
|
||||||
Connected = CanConnect;
|
Connected = CanConnect;
|
||||||
|
|||||||
@@ -50,13 +50,17 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// <param name="name">Method name for looking up json test values</param>
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
/// <param name="ignoreProperties">Ignore certain properties</param>
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useFirstUpdateItem">Use the first item of an array update</param>
|
||||||
|
/// <param name="addressPath">Path</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="Exception"></exception>
|
/// <exception cref="Exception"></exception>
|
||||||
public async Task ValidateAsync<TUpdate>(
|
public async Task ValidateAsync<TUpdate>(
|
||||||
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
||||||
string name,
|
string name,
|
||||||
string? nestedJsonProperty = null,
|
string? nestedJsonProperty = null,
|
||||||
List<string>? ignoreProperties = null)
|
List<string>? ignoreProperties = null,
|
||||||
|
string? addressPath = null,
|
||||||
|
bool? useFirstUpdateItem = null)
|
||||||
{
|
{
|
||||||
var listener = new EnumValueTraceListener();
|
var listener = new EnumValueTraceListener();
|
||||||
Trace.Listeners.Add(listener);
|
Trace.Listeners.Add(listener);
|
||||||
@@ -79,7 +83,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
var data = Encoding.UTF8.GetString(buffer);
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
using var reader = new StringReader(data);
|
using var reader = new StringReader(data);
|
||||||
|
|
||||||
var socket = TestHelpers.ConfigureSocketClient(_client);
|
var socket = TestHelpers.ConfigureSocketClient(_client, addressPath == null ? _baseAddress : _baseAddress.AppendPath(addressPath));
|
||||||
|
|
||||||
var waiter = new AutoResetEvent(false);
|
var waiter = new AutoResetEvent(false);
|
||||||
string? lastMessage = null;
|
string? lastMessage = null;
|
||||||
@@ -157,9 +161,9 @@ namespace CryptoExchange.Net.Testing
|
|||||||
throw new Exception($"{name} Update send to client did not trigger in update handler");
|
throw new Exception($"{name} Update send to client did not trigger in update handler");
|
||||||
|
|
||||||
if (_stjCompare == true)
|
if (_stjCompare == true)
|
||||||
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
|
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||||
else
|
else
|
||||||
JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
|
JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ namespace CryptoExchange.Net.Testing
|
|||||||
return self == to;
|
return self == to;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TestSocket ConfigureSocketClient<T>(T client) where T : BaseSocketClient
|
internal static TestSocket ConfigureSocketClient<T>(T client, string address) where T : BaseSocketClient
|
||||||
{
|
{
|
||||||
var socket = new TestSocket();
|
var socket = new TestSocket(address);
|
||||||
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
||||||
{
|
{
|
||||||
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ The following API's are directly supported. Note that there are 3rd party implem
|
|||||||
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|
|
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|
|
||||||
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|
|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|
|
||||||
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|
|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|
|
||||||
|
|Gate.io|[JKorf/GateIo.Net](https://github.com/JKorf/GateIo.Net)|[](https://www.nuget.org/packages/GateIo.Net)|
|
||||||
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[](https://www.nuget.org/packages/Huobi.Net)|
|
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[](https://www.nuget.org/packages/Huobi.Net)|
|
||||||
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[](https://www.nuget.org/packages/KrakenExchange.Net)|
|
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[](https://www.nuget.org/packages/KrakenExchange.Net)|
|
||||||
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|
|
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|
|
||||||
@@ -45,6 +46,41 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
|||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 7.7.2 - 25 Jun 2024
|
||||||
|
* Fixed ratelimiting issue possibly creating negative delays
|
||||||
|
|
||||||
|
* Version 7.7.1 - 23 Jun 2024
|
||||||
|
* Fixes for caching implementation
|
||||||
|
|
||||||
|
* Version 7.7.0 - 23 Jun 2024
|
||||||
|
* Caching support
|
||||||
|
* Caching is supported for GET requests within a certain time frame
|
||||||
|
* Enable caching by setting CachingEnabled to true in the client options
|
||||||
|
* Added DataSource to CallResult object
|
||||||
|
* Dedicated websocket connection
|
||||||
|
* Added functionality for always having a connection open which can then be used for order operations
|
||||||
|
* This eliminates the initial connection time for the first request
|
||||||
|
* WebSocket connection can be prepared by calling PrepareConnectionsAsync on the Api client, for example `await binanceSocketClient.SpotApi.PrepareConnectionsAsync()`. This is only needed initially; it will be reconnected when connection is lost.
|
||||||
|
* Added CancellationToken support for websocket queries
|
||||||
|
* Added SocketConnection parameter to SocketApiClient.GetAuthenticationRequest method
|
||||||
|
* Added ObjectStringConverter base converter for deserializing nested json strings
|
||||||
|
* Fixed websocket issue with ratelimiting and reconnecting interaction
|
||||||
|
* Fixed rate limiting issue with sub-millisecond delays
|
||||||
|
* Fixed websocket connection will now close if authentication fails because of not set credentials
|
||||||
|
* Updated websocket reconnection handling and options, added backoff policy
|
||||||
|
* Removed check for confirmed subscription as data often is pushed before the subscription is confirmed
|
||||||
|
|
||||||
|
* Version 7.6.0 - 11 Jun 2024
|
||||||
|
* Added support for specifying seperate uri and body parameters
|
||||||
|
* Added support for different message and handling generic types on socket queries
|
||||||
|
* Added support for PATCH http method requests
|
||||||
|
* Added support for setting http request body to a specific type directly
|
||||||
|
* Split DataEvent.Topic into StreamId and Symbol properties
|
||||||
|
* Added support for negative time values parsing
|
||||||
|
* Added some helper methods for converting DataEvent to CallResult
|
||||||
|
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
|
||||||
|
* Updated some testing methods
|
||||||
|
|
||||||
* Version 7.5.2 - 07 May 2024
|
* Version 7.5.2 - 07 May 2024
|
||||||
* Fixed SetApiCredentials not correctly being used by rate limiter causing exception
|
* Fixed SetApiCredentials not correctly being used by rate limiter causing exception
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -136,18 +136,19 @@
|
|||||||
<th>Repository</th>
|
<th>Repository</th>
|
||||||
<th>Nuget</th>
|
<th>Nuget</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr><td>Binance</td><td><a href="https://github.com/JKorf/Binance.Net">JKorf/Binance.Net</a></td><td><a href="https://www.nuget.org/packages/Binance.Net"><img src="https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Binance</td><td><a href="https://github.com/JKorf/Binance.Net">JKorf/Binance.Net</a></td><td><a href="https://www.nuget.org/packages/Binance.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Huobi</td><td><a href="https://github.com/JKorf/Huobi.Net">JKorf/Huobi.Net</a></td><td><a href="https://www.nuget.org/packages/Huobi.Net"><img src="https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Gate.io</td><td><a href="https://github.com/JKorf/GateIo.Net">JKorf/GateIo.Net</a></td><td><a href="https://www.nuget.org/packages/GateIo.Net" target="_blank"><img src="https://img.shields.io/nuget/v/GateIo.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Kraken</td><td><a href="https://github.com/JKorf/Kraken.Net">JKorf/Kraken.Net</a></td><td><a href="https://www.nuget.org/packages/KrakenExchange.Net"><img src="https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Huobi</td><td><a href="https://github.com/JKorf/Huobi.Net">JKorf/Huobi.Net</a></td><td><a href="https://www.nuget.org/packages/Huobi.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Kucoin</td><td><a href="https://github.com/JKorf/Kucoin.Net">JKorf/Kucoin.Net</a></td><td><a href="https://www.nuget.org/packages/Kucoin.Net"><img src="https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Kraken</td><td><a href="https://github.com/JKorf/Kraken.Net">JKorf/Kraken.Net</a></td><td><a href="https://www.nuget.org/packages/KrakenExchange.Net" target="_blank"><img src="https://img.shields.io/nuget/v/KrakenExchange.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"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Kucoin</td><td><a href="https://github.com/JKorf/Kucoin.Net">JKorf/Kucoin.Net</a></td><td><a href="https://www.nuget.org/packages/Kucoin.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Kucoin.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"><img src="https://img.shields.io/nuget/v/JK.OKX.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>
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user