mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f30c72608 | |||
| e927bc3d20 | |||
| 09ed7d1436 | |||
| 6fed657ea6 | |||
| 1555f8da0c | |||
| 68b28fc875 | |||
| 5d50d8cde8 | |||
| 9ff673d8be | |||
| 3e5a34fb56 | |||
| 64ee50d98c | |||
| 6a105c6f8f | |||
| 287aadc720 | |||
| 7229438a0b | |||
| 444af98a15 | |||
| 70c6fa1bbb | |||
| d27f394b46 | |||
| c8c98e13d0 |
@@ -72,7 +72,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
result = messageEvent.Data;
|
||||
rstEvent.Set();
|
||||
});
|
||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
||||
sub.AddSubscription(subObj);
|
||||
|
||||
// act
|
||||
@@ -107,7 +106,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
});
|
||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
||||
sub.AddSubscription(subObj);
|
||||
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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Caching;
|
||||
using CryptoExchange.Net.Converters.JsonNet;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
@@ -17,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
@@ -85,6 +87,10 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <inheritdoc />
|
||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Memory cache
|
||||
/// </summary>
|
||||
private static MemoryCache _cache = new MemoryCache();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -190,6 +196,21 @@ namespace CryptoExchange.Net.Clients
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
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;
|
||||
while (true)
|
||||
{
|
||||
@@ -215,6 +236,12 @@ namespace CryptoExchange.Net.Clients
|
||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
if (result.Success &&
|
||||
ShouldCache(definition))
|
||||
{
|
||||
_cache.Add(key, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -445,6 +472,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="requestWeight">Credits used for 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="preventCaching">Whether caching should be prevented for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||
@@ -458,9 +486,25 @@ namespace CryptoExchange.Net.Clients
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
IRateLimitGate? gate = null
|
||||
IRateLimitGate? gate = null,
|
||||
bool preventCaching = false
|
||||
) 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;
|
||||
while (true)
|
||||
{
|
||||
@@ -478,6 +522,13 @@ namespace CryptoExchange.Net.Clients
|
||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
if (result.Success &&
|
||||
ShouldCache(method) &&
|
||||
!preventCaching)
|
||||
{
|
||||
_cache.Add(key, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -949,5 +1000,14 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, 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 Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -52,11 +53,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
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>
|
||||
/// The rate limiters
|
||||
/// </summary>
|
||||
@@ -72,6 +68,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
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 />
|
||||
public double IncomingKbps
|
||||
{
|
||||
@@ -136,6 +137,16 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
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>
|
||||
/// Add a query to periodically send on each connection
|
||||
/// </summary>
|
||||
@@ -198,12 +209,11 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// 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)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
var success = socketConnection.AddSubscription(subscription);
|
||||
@@ -250,11 +260,18 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!subResult)
|
||||
{
|
||||
waitEvent?.Set();
|
||||
_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
|
||||
var unsubscribe = subResult.Error is CancellationRequestedError;
|
||||
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||
if (isTimeout && subscription.Confirmed)
|
||||
{
|
||||
// No response received, but the subscription did receive updates. We'll assume success
|
||||
}
|
||||
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!);
|
||||
@@ -281,10 +298,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||
/// <param name="query">The query</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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>
|
||||
@@ -294,18 +312,22 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||
/// <param name="url">The url for the request</param>
|
||||
/// <param name="query">The query</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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)
|
||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<THandlerResponse>(default);
|
||||
|
||||
@@ -334,7 +356,10 @@ namespace CryptoExchange.Net.Clients
|
||||
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>
|
||||
@@ -358,7 +383,11 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!authenticated || socket.Authenticated)
|
||||
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>
|
||||
@@ -372,7 +401,7 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
var authRequest = GetAuthenticationRequest();
|
||||
var authRequest = GetAuthenticationRequest(socket);
|
||||
if (authRequest != null)
|
||||
{
|
||||
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
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
|
||||
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
@@ -446,19 +475,31 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket is for</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>
|
||||
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)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& s.Value.ApiClient.GetType() == GetType()
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
|
||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||
if (result != null)
|
||||
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.ApiClient.GetType() == GetType()
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||
&& s.Value.Connected);
|
||||
|
||||
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
|
||||
return new CallResult<SocketConnection>(result);
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
}
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
@@ -475,6 +516,7 @@ namespace CryptoExchange.Net.Clients
|
||||
var socket = CreateSocket(connectionAddress.Data!);
|
||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||
|
||||
foreach (var ptg in PeriodicTaskRegistrations)
|
||||
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>
|
||||
/// <returns></returns>
|
||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
||||
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||
{
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
@@ -594,8 +636,8 @@ namespace CryptoExchange.Net.Clients
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||
tasks.Add(connection.CloseAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
@@ -618,6 +660,23 @@ namespace CryptoExchange.Net.Clients
|
||||
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>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
@@ -701,11 +760,18 @@ namespace CryptoExchange.Net.Clients
|
||||
public override void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
_logger.DisposingSocketClient();
|
||||
_ = UnsubscribeAllAsync();
|
||||
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||
if (socketList.Any())
|
||||
_logger.DisposingSocketClient();
|
||||
|
||||
foreach (var connection in socketList)
|
||||
{
|
||||
tasks.Add(connection.CloseAsync());
|
||||
}
|
||||
}
|
||||
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("1");
|
||||
throw new Exception("Not an array");
|
||||
|
||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
||||
attributes = CacheTypeAttributes(objectType);
|
||||
@@ -92,8 +92,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||
var targetType = attribute.PropertyInfo.PropertyType;
|
||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
var targetType = attribute.PropertyInfo.PropertyType;
|
||||
|
||||
object? value = 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>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>7.5.2</PackageVersion>
|
||||
<AssemblyVersion>7.5.2</AssemblyVersion>
|
||||
<FileVersion>7.5.2</FileVersion>
|
||||
<PackageVersion>7.7.0</PackageVersion>
|
||||
<AssemblyVersion>7.7.0</AssemblyVersion>
|
||||
<FileVersion>7.7.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -96,6 +96,9 @@ namespace CryptoExchange.Net
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
if (kvp.Value is null)
|
||||
continue;
|
||||
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
|
||||
@@ -16,10 +16,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
/// <summary>
|
||||
/// Whether this listener can handle data
|
||||
/// </summary>
|
||||
public bool CanHandleData { get; }
|
||||
/// <summary>
|
||||
/// The identifiers for this processor
|
||||
/// </summary>
|
||||
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 System.Threading.Tasks;
|
||||
|
||||
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
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?> _restApiRateLimitPauseUntil;
|
||||
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()
|
||||
@@ -65,6 +68,21 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Debug,
|
||||
new EventId(4008, "RestApiSendRequest"),
|
||||
"[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)
|
||||
@@ -111,5 +129,20 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_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
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<bool> WaitAsync(TimeSpan? timeout = null)
|
||||
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
||||
{
|
||||
lock (_waits)
|
||||
{
|
||||
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
else
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if(timeout != null)
|
||||
{
|
||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
||||
var registration = cancellationSource.Token.Register(() =>
|
||||
{
|
||||
lock (_waits)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
if (ct.IsCancellationRequested)
|
||||
return _completed;
|
||||
|
||||
// Not the cleanest but it works
|
||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||
}
|
||||
}, useSynchronizationContext: false);
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (timeout.HasValue)
|
||||
{
|
||||
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);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
@@ -331,6 +331,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data source of this result
|
||||
/// </summary>
|
||||
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new result
|
||||
/// </summary>
|
||||
@@ -417,6 +422,17 @@ namespace CryptoExchange.Net.Objects
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a copy of this result with data source set to cache
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal WebCallResult<T> Cached()
|
||||
{
|
||||
var result = new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, Error);
|
||||
result.DataSource = ResultDataSource.Cache;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -169,4 +169,38 @@
|
||||
/// </summary>
|
||||
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>
|
||||
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>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
@@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
CachingEnabled = CachingEnabled,
|
||||
CachingMaxAge = CachingMaxAge,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
@@ -9,15 +10,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public class SocketExchangeOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect policy
|
||||
/// </summary>
|
||||
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoReconnect = AutoReconnect,
|
||||
ReconnectPolicy = ReconnectPolicy,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
|
||||
@@ -61,6 +61,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this request should never be cached
|
||||
/// </summary>
|
||||
public bool PreventCaching { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="requestBodyFormat">Request body format</param>
|
||||
/// <param name="parameterPosition">Parameter position</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
HttpMethod method,
|
||||
@@ -59,7 +60,8 @@ namespace CryptoExchange.Net.Objects
|
||||
TimeSpan? endpointLimitPeriod = null,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null)
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null)
|
||||
{
|
||||
|
||||
if (!_definitions.TryGetValue(method + path, out var def))
|
||||
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ArraySerialization = arraySerialization,
|
||||
RequestBodyFormat = requestBodyFormat,
|
||||
ParameterPosition = parameterPosition,
|
||||
PreventCaching = preventCaching ?? false
|
||||
};
|
||||
_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>();
|
||||
|
||||
/// <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>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect policy
|
||||
/// </summary>
|
||||
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for the connection
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the socket should automatically reconnect when connection is lost
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||
/// </summary>
|
||||
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="uri">Uri</param>
|
||||
/// <param name="autoReconnect">Auto reconnect</param>
|
||||
public WebSocketParameters(Uri uri, bool autoReconnect)
|
||||
/// <param name="policy">Reconnect policy</param>
|
||||
public WebSocketParameters(Uri uri, ReconnectPolicy policy)
|
||||
{
|
||||
Uri = uri;
|
||||
AutoReconnect = autoReconnect;
|
||||
ReconnectPolicy = policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
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));
|
||||
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);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
private ProcessState _processState;
|
||||
private DateTime _lastReconnectTime;
|
||||
private string _baseAddress;
|
||||
private int _reconnectAttempt;
|
||||
|
||||
private const int _receiveBufferSize = 1048576;
|
||||
private const int _sendBufferSize = 4096;
|
||||
@@ -246,12 +247,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
_closeTask = null;
|
||||
|
||||
if (!Parameters.AutoReconnect)
|
||||
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
|
||||
{
|
||||
_processState = ProcessState.Idle;
|
||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_stopRequested)
|
||||
{
|
||||
@@ -259,9 +260,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
||||
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
||||
// Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected
|
||||
var initialDelay = GetReconnectDelay();
|
||||
await Task.Delay(initialDelay).ConfigureAwait(false);
|
||||
|
||||
while (!_stopRequested)
|
||||
{
|
||||
@@ -282,13 +283,17 @@ namespace CryptoExchange.Net.Sockets
|
||||
_ctsSource = new CancellationTokenSource();
|
||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||
|
||||
_reconnectAttempt++;
|
||||
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||
if (!connected)
|
||||
{
|
||||
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
||||
// Delay between reconnect attempts
|
||||
var delay = GetReconnectDelay();
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
_reconnectAttempt = 0;
|
||||
_lastReconnectTime = DateTime.UtcNow;
|
||||
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
break;
|
||||
@@ -298,6 +303,24 @@ namespace CryptoExchange.Net.Sockets
|
||||
_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 />
|
||||
public virtual void Send(int id, string data, int weight)
|
||||
{
|
||||
@@ -439,11 +462,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
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);
|
||||
if (!limitResult)
|
||||
try
|
||||
{
|
||||
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
continue;
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
||||
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>
|
||||
public int Id { get; } = ExchangeHelpers.NextId();
|
||||
|
||||
/// <summary>
|
||||
/// Can handle data
|
||||
/// </summary>
|
||||
public bool CanHandleData => true;
|
||||
|
||||
/// <summary>
|
||||
/// Has this query been completed
|
||||
/// </summary>
|
||||
@@ -116,8 +111,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Wait untill timeout or the request is competed
|
||||
/// </summary>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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 />
|
||||
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 readonly object _listenersLock;
|
||||
private readonly List<IMessageProcessor> _listeners;
|
||||
@@ -443,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// 4. Get the listeners interested in this message
|
||||
List<IMessageProcessor> processors;
|
||||
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)
|
||||
{
|
||||
@@ -451,7 +456,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
List<string> listenerIds;
|
||||
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));
|
||||
UnhandledMessage?.Invoke(_accessor);
|
||||
}
|
||||
@@ -478,6 +483,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
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
|
||||
object? deserialized = null;
|
||||
desCache?.TryGetValue(messageType, out deserialized);
|
||||
@@ -604,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
bool shouldCloseConnection;
|
||||
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)
|
||||
Status = SocketStatus.Closing;
|
||||
}
|
||||
@@ -686,10 +695,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
/// <param name="query">Query to send</param>
|
||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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"));
|
||||
}
|
||||
|
||||
@@ -700,14 +710,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||
/// <param name="query">Query to send</param>
|
||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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"));
|
||||
}
|
||||
|
||||
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent)
|
||||
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent, CancellationToken ct = default)
|
||||
{
|
||||
lock(_listenersLock)
|
||||
_listeners.Add(query);
|
||||
@@ -724,7 +735,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
if (!_socket.IsOpen)
|
||||
{
|
||||
@@ -735,11 +746,17 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (query.Completed)
|
||||
return;
|
||||
|
||||
await query.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
|
||||
await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
|
||||
|
||||
if (query.Completed)
|
||||
return;
|
||||
}
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
query.Fail(new CancellationRequestedError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -799,20 +816,23 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket not connected"));
|
||||
|
||||
bool anySubscriptions;
|
||||
lock (_listenersLock)
|
||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||
if (!anySubscriptions)
|
||||
if (!DedicatedRequestConnection)
|
||||
{
|
||||
// No need to resubscribe anything
|
||||
_logger.NothingToResubscribeCloseConnection(SocketId);
|
||||
_ = _socket.CloseAsync();
|
||||
return new CallResult(null);
|
||||
bool anySubscriptions;
|
||||
lock (_listenersLock)
|
||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||
if (!anySubscriptions)
|
||||
{
|
||||
// No need to resubscribe anything
|
||||
_logger.NothingToResubscribeCloseConnection(SocketId);
|
||||
_ = _socket.CloseAsync();
|
||||
return new CallResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
bool anyAuthenticated;
|
||||
lock (_listenersLock)
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated);
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
|
||||
if (anyAuthenticated)
|
||||
{
|
||||
// If we reconnected a authenticated connection we need to re-authenticate
|
||||
@@ -861,6 +881,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
waitEvent.Set();
|
||||
if (r.Result.Success)
|
||||
subscription.Confirmed = true;
|
||||
return r.Result;
|
||||
}));
|
||||
}
|
||||
@@ -870,9 +892,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
return taskList.First(t => !t.Result.Success).Result;
|
||||
}
|
||||
|
||||
foreach (var subscription in subList)
|
||||
subscription.Confirmed = true;
|
||||
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket not connected"));
|
||||
|
||||
|
||||
@@ -18,11 +18,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Can handle data
|
||||
/// </summary>
|
||||
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of invocations
|
||||
/// </summary>
|
||||
@@ -42,11 +37,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Has the subscription been confirmed
|
||||
/// </summary>
|
||||
public bool Confirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this subscription should handle update messages before confirmation
|
||||
/// </summary>
|
||||
public bool HandleUpdatesBeforeConfirmation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription closed
|
||||
|
||||
@@ -30,9 +30,14 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
public bool IsClosed => !Connected;
|
||||
public bool IsOpen => Connected;
|
||||
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 TestSocket(string address)
|
||||
{
|
||||
Uri = new Uri(address);
|
||||
}
|
||||
|
||||
public Task<CallResult> ConnectAsync()
|
||||
{
|
||||
Connected = CanConnect;
|
||||
|
||||
@@ -50,13 +50,17 @@ namespace CryptoExchange.Net.Testing
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="nestedJsonProperty">Use nested json property for compare</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>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task ValidateAsync<TUpdate>(
|
||||
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
||||
string name,
|
||||
string? nestedJsonProperty = null,
|
||||
List<string>? ignoreProperties = null)
|
||||
List<string>? ignoreProperties = null,
|
||||
string? addressPath = null,
|
||||
bool? useFirstUpdateItem = null)
|
||||
{
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
@@ -79,7 +83,7 @@ namespace CryptoExchange.Net.Testing
|
||||
var data = Encoding.UTF8.GetString(buffer);
|
||||
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);
|
||||
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");
|
||||
|
||||
if (_stjCompare == true)
|
||||
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
|
||||
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||
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;
|
||||
}
|
||||
|
||||
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>())
|
||||
{
|
||||
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)|
|
||||
|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)|
|
||||
|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)|
|
||||
|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)|
|
||||
@@ -45,6 +46,35 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 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
|
||||
* Fixed SetApiCredentials not correctly being used by rate limiter causing exception
|
||||
|
||||
|
||||
+13
-12
@@ -136,18 +136,19 @@
|
||||
<th>Repository</th>
|
||||
<th>Nuget</th>
|
||||
</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>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>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>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>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>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>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>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>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>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>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>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>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" 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" 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" 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" 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" 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" target="_blank"><img src="https://img.shields.io/nuget/v/CoinGecko.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>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>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>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>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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user