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

Compare commits

...

4 Commits

19 changed files with 273 additions and 231 deletions
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
public override TimeSpan GetTimeOffset() => throw new NotImplementedException();
public override TimeSyncInfo GetTimeSyncInfo() => throw new NotImplementedException();
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
}
@@ -142,7 +142,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position;
}
public override TimeSpan GetTimeOffset()
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
@@ -178,7 +178,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
}
public override TimeSpan GetTimeOffset()
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
@@ -21,20 +21,6 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
public SecureString? Secret { get; }
/// <summary>
/// The private key to authenticate requests
/// </summary>
public PrivateKey? PrivateKey { get; }
/// <summary>
/// Create Api credentials providing a private key for authentication
/// </summary>
/// <param name="privateKey">The private key used for signing</param>
public ApiCredentials(PrivateKey privateKey)
{
PrivateKey = privateKey;
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
@@ -69,11 +55,8 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
public virtual ApiCredentials Copy()
{
if (PrivateKey == null)
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
else
return new ApiCredentials(PrivateKey!.Copy());
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
}
/// <summary>
@@ -123,7 +106,6 @@ namespace CryptoExchange.Net.Authentication
{
Key?.Dispose();
Secret?.Dispose();
PrivateKey?.Dispose();
}
}
}
@@ -223,7 +223,7 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
protected static DateTime GetTimestamp(RestApiClient apiClient)
{
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
}
/// <summary>
@@ -1,110 +0,0 @@
using System;
using System.Security;
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Private key info
/// </summary>
public class PrivateKey : IDisposable
{
/// <summary>
/// The private key
/// </summary>
public SecureString Key { get; }
/// <summary>
/// The private key's pass phrase
/// </summary>
public SecureString? Passphrase { get; }
/// <summary>
/// Indicates if the private key is encrypted or not
/// </summary>
public bool IsEncrypted { get; }
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
/// <param name="passphrase">The private key's passphrase</param>
public PrivateKey(SecureString key, SecureString passphrase)
{
Key = key;
Passphrase = passphrase;
IsEncrypted = true;
}
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
/// <param name="passphrase">The private key's passphrase</param>
public PrivateKey(string key, string passphrase)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
throw new ArgumentException("Key and passphrase can't be null/empty");
var secureKey = new SecureString();
foreach (var c in key)
secureKey.AppendChar(c);
secureKey.MakeReadOnly();
Key = secureKey;
var securePassphrase = new SecureString();
foreach (var c in passphrase)
securePassphrase.AppendChar(c);
securePassphrase.MakeReadOnly();
Passphrase = securePassphrase;
IsEncrypted = true;
}
/// <summary>
/// Create a private key providing an unencrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
public PrivateKey(SecureString key)
{
Key = key;
IsEncrypted = false;
}
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
public PrivateKey(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key can't be null/empty");
Key = key.ToSecureString();
IsEncrypted = false;
}
/// <summary>
/// Copy the private key
/// </summary>
/// <returns></returns>
public PrivateKey Copy()
{
if (Passphrase == null)
return new PrivateKey(Key.GetString());
else
return new PrivateKey(Key.GetString(), Passphrase.GetString());
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Passphrase?.Dispose();
}
}
}
+5 -4
View File
@@ -6,6 +6,7 @@ using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
@@ -17,7 +18,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Base API for all API clients
/// </summary>
public abstract class BaseApiClient: IDisposable
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
private ApiCredentials? _apiCredentials;
private AuthenticationProvider? _authenticationProvider;
@@ -38,7 +39,7 @@ namespace CryptoExchange.Net
/// </summary>
public AuthenticationProvider? AuthenticationProvider
{
get
get
{
if (!_created && !_disposing && _apiCredentials != null)
{
@@ -98,7 +99,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Lock for id generating
/// </summary>
protected static object idLock = new ();
protected static object idLock = new();
/// <summary>
/// A default serializer
@@ -131,7 +132,7 @@ namespace CryptoExchange.Net
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc />
public void SetApiCredentials(ApiCredentials credentials)
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
_apiCredentials = credentials?.Copy();
_created = false;
+1 -1
View File
@@ -53,7 +53,7 @@ namespace CryptoExchange.Net
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public void SetApiCredentials(ApiCredentials credentials)
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
+72 -47
View File
@@ -20,35 +20,24 @@ namespace CryptoExchange.Net
/// <summary>
/// Base rest API client for interacting with a REST API
/// </summary>
public abstract class RestApiClient: BaseApiClient
public abstract class RestApiClient : BaseApiClient, IRestApiClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary>
/// Get time sync info for an API client
/// </summary>
/// <returns></returns>
public abstract TimeSyncInfo GetTimeSyncInfo();
/// <summary>
/// Get time offset for an API client
/// </summary>
/// <returns></returns>
public abstract TimeSpan GetTimeOffset();
/// <summary>
/// Total amount of requests made with this API client
/// </summary>
public int TotalRequestsMade { get; set; }
/// <summary>
/// Options for this client
/// </summary>
@@ -70,7 +59,7 @@ namespace CryptoExchange.Net
/// <param name="log">Logger</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions): base(log, options, apiOptions)
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions) : base(log, options, apiOptions)
{
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
@@ -110,12 +99,20 @@ namespace CryptoExchange.Net
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
return result.AsDataless();
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
return result.AsDataless();
}
}
/// <summary>
@@ -149,11 +146,20 @@ namespace CryptoExchange.Net
bool ignoreRatelimit = false
) where T : class
{
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
return await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
return result;
}
}
/// <summary>
@@ -190,7 +196,8 @@ namespace CryptoExchange.Net
{
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo.TimeSyncState.LastSyncTime == default)
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeResult = await syncTask.ConfigureAwait(false);
@@ -235,8 +242,6 @@ namespace CryptoExchange.Net
return new CallResult<IRequest>(request);
}
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
@@ -376,6 +381,16 @@ namespace CryptoExchange.Net
return Task.FromResult<ServerError?>(null);
}
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
/// Note that this is always called; even when the request might be successful
/// </summary>
/// <typeparam name="T">WebCallResult type parameter</typeparam>
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
/// <summary>
/// Creates a request object
/// </summary>
@@ -418,17 +433,24 @@ namespace CryptoExchange.Net
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
if (AuthenticationProvider != null)
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
method,
parameters,
signed,
arraySerialization,
parameterPosition,
out uriParameters,
out bodyParameters,
out headers);
try
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
method,
parameters,
signed,
arraySerialization,
parameterPosition,
out uriParameters,
out bodyParameters,
out headers);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
}
// Sanity check
@@ -514,11 +536,14 @@ namespace CryptoExchange.Net
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary>
/// <returns>Server time</returns>
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
@@ -550,7 +575,7 @@ namespace CryptoExchange.Net
// Calculate time offset between local and server
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
@@ -18,13 +18,10 @@ namespace CryptoExchange.Net
/// <summary>
/// Base socket API client for interaction with a websocket API
/// </summary>
public abstract class SocketApiClient : BaseApiClient
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{
#region Fields
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// </summary>
/// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <summary>
@@ -117,7 +114,7 @@ namespace CryptoExchange.Net
/// <param name="log">log</param>
/// <param name="options">Client options</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions): base(log, options, apiOptions)
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions) : base(log, options, apiOptions)
{
ClientOptions = options;
}
@@ -265,7 +262,7 @@ namespace CryptoExchange.Net
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{
CallResult<object>? callResult = null;
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true)
{
@@ -351,7 +348,7 @@ namespace CryptoExchange.Net
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
{
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, data =>
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, null, data =>
{
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false;
+4 -4
View File
@@ -6,16 +6,16 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>5.3.1</PackageVersion>
<AssemblyVersion>5.3.1</AssemblyVersion>
<FileVersion>5.3.1</FileVersion>
<PackageVersion>5.4.0</PackageVersion>
<AssemblyVersion>5.4.0</AssemblyVersion>
<FileVersion>5.4.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>5.3.1 - Added default request parameter ordering before applying authentication, Fixed possible issue where a socket would reconnect when it should close if it was already in reconnecting</PackageReleaseNotes>
<PackageReleaseNotes>5.4.0 - Added unsubscribing when receiving subscribe answer after the request timeout has passed, Fixed socket options copying, Made TimeSync implementation optional, Cleaned up ApiCredentials and added better support for extending ApiCredentials</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>9.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@@ -0,0 +1,17 @@
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Base api client
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Set the API credentials for this API client
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
}
}
@@ -0,0 +1,33 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Base rest API client
/// </summary>
public interface IRestApiClient : IBaseApiClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
IRequestFactory RequestFactory { get; set; }
/// <summary>
/// Total amount of requests made with this API client
/// </summary>
int TotalRequestsMade { get; set; }
/// <summary>
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
/// </summary>
/// <returns></returns>
TimeSpan? GetTimeOffset();
/// <summary>
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
/// </summary>
/// <returns></returns>
TimeSyncInfo? GetTimeSyncInfo();
}
}
@@ -18,11 +18,5 @@ namespace CryptoExchange.Net.Interfaces
/// The total amount of requests made with this client
/// </summary>
int TotalRequestsMade { get; }
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(ApiCredentials credentials);
}
}
@@ -0,0 +1,81 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Socket API client
/// </summary>
public interface ISocketApiClient: IBaseApiClient
{
/// <summary>
/// The current amount of socket connections on the API client
/// </summary>
int CurrentConnections { get; }
/// <summary>
/// The current amount of subscriptions over all connections
/// </summary>
int CurrentSubscriptions { get; }
/// <summary>
/// Incoming data kpbs
/// </summary>
double IncomingKbps { get; }
/// <summary>
/// Client options
/// </summary>
SocketApiClientOptions Options { get; }
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
/// <summary>
/// Get the url to reconnect to after losing a connection
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
string GetSubscriptionsState();
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
Task ReconnectAsync();
/// <summary>
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// </summary>
/// <param name="request">The original request</param>
/// <returns></returns>
Task<CallResult<object>> RevitalizeRequestAsync(object request);
/// <summary>
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="identifier">Identifier for the periodic send</param>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
Task UnsubscribeAllAsync();
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
Task<bool> UnsubscribeAsync(int subscriptionId);
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(UpdateSubscription subscription);
}
}
@@ -16,12 +16,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
ClientOptions ClientOptions { get; }
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(ApiCredentials credentials);
/// <summary>
/// Incoming kilobytes per second of data
/// </summary>
+8 -8
View File
@@ -298,14 +298,14 @@ namespace CryptoExchange.Net.Objects
if (baseOptions == null)
return;
AutoReconnect = baseOptions.AutoReconnect;
ReconnectInterval = baseOptions.ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
MaxSocketConnections = baseOptions.MaxSocketConnections;
DelayAfterConnect = baseOptions.DelayAfterConnect;
AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
}
/// <inheritdoc />
+13 -9
View File
@@ -11,15 +11,19 @@ namespace CryptoExchange.Net.Sockets
public JToken? Result { get; private set; }
public bool Completed { get; private set; }
public AsyncResetEvent Event { get; }
public DateTime RequestTimestamp { get; set; }
public TimeSpan Timeout { get; }
public SocketSubscription? Subscription { get; }
private CancellationTokenSource cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
{
Handler = handler;
Event = new AsyncResetEvent(false, false);
Timeout = timeout;
RequestTimestamp = DateTime.UtcNow;
Subscription = subscription;
cts = new CancellationTokenSource(timeout);
cts.Token.Register(Fail, false);
@@ -27,15 +31,15 @@ namespace CryptoExchange.Net.Sockets
public bool CheckData(JToken data)
{
if (Handler(data))
{
Result = data;
Completed = true;
Event.Set();
return true;
}
return Handler(data);
}
return false;
public bool Succeed(JToken data)
{
Result = data;
Completed = true;
Event.Set();
return true;
}
public void Fail()
+21 -3
View File
@@ -304,18 +304,35 @@ namespace CryptoExchange.Net.Sockets
PendingRequest[] requests;
lock (_pendingRequests)
{
_pendingRequests.RemoveAll(r => r.Completed);
// Remove only timed out requests after 5 minutes have passed so we can still process any
// message coming in after the request timeout
_pendingRequests.RemoveAll(r => r.Completed && DateTime.UtcNow - r.RequestTimestamp > TimeSpan.FromMinutes(5));
requests = _pendingRequests.ToArray();
}
// Check if this message is an answer on any pending requests
foreach (var pendingRequest in requests)
{
if (pendingRequest.CheckData(tokenData))
{
lock (_pendingRequests)
_pendingRequests.Remove(pendingRequest);
if (pendingRequest.Completed)
{
// Answer to a timed out request, unsub if it is a subscription request
if (pendingRequest.Subscription != null)
{
_log.Write(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout");
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
}
}
else
{
pendingRequest.Succeed(tokenData);
}
if (!ApiClient.ContinueOnQueryResponse)
return;
@@ -546,11 +563,12 @@ namespace CryptoExchange.Net.Sockets
/// <typeparam name="T">The data type expected in response</typeparam>
/// <param name="obj">The object to send</param>
/// <param name="timeout">The timeout for response</param>
/// <param name="subscription">Subscription if this is a subscribe request</param>
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
/// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, Func<JToken, bool> handler)
{
var pending = new PendingRequest(handler, timeout);
var pending = new PendingRequest(handler, timeout, subscription);
lock (_pendingRequests)
{
_pendingRequests.Add(pending);
+6
View File
@@ -33,6 +33,12 @@ 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 5.4.0 - 14 Feb 2023
* Added unsubscribing when receiving subscribe answer after the request timeout has passed
* Fixed socket options copying
* Made TimeSync implementation optional
* Cleaned up ApiCredentials and added better support for extending ApiCredentials
* Version 5.3.1 - 08 Dec 2022
* Added default request parameter ordering before applying authentication
* Fixed possible issue where a socket would reconnect when it should close if it was already in reconnecting