diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 0b26388..81b7d56 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests public CallResult Deserialize(string data) => Deserialize(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> GetServerTimestampAsync() => throw new NotImplementedException(); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 3dbf67b..cce6de6 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -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(); } diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index c3e4523..b5f1c0a 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -21,20 +21,6 @@ namespace CryptoExchange.Net.Authentication /// public SecureString? Secret { get; } - /// - /// The private key to authenticate requests - /// - public PrivateKey? PrivateKey { get; } - - /// - /// Create Api credentials providing a private key for authentication - /// - /// The private key used for signing - public ApiCredentials(PrivateKey privateKey) - { - PrivateKey = privateKey; - } - /// /// Create Api credentials providing an api key and secret for authentication /// @@ -69,11 +55,8 @@ namespace CryptoExchange.Net.Authentication /// 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()); } /// @@ -123,7 +106,6 @@ namespace CryptoExchange.Net.Authentication { Key?.Dispose(); Secret?.Dispose(); - PrivateKey?.Dispose(); } } } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 2942d9e..770ea90 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -223,7 +223,7 @@ namespace CryptoExchange.Net.Authentication /// protected static DateTime GetTimestamp(RestApiClient apiClient) { - return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!; + return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!; } /// diff --git a/CryptoExchange.Net/Authentication/PrivateKey.cs b/CryptoExchange.Net/Authentication/PrivateKey.cs deleted file mode 100644 index 238e9b8..0000000 --- a/CryptoExchange.Net/Authentication/PrivateKey.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Security; - -namespace CryptoExchange.Net.Authentication -{ - /// - /// Private key info - /// - public class PrivateKey : IDisposable - { - /// - /// The private key - /// - public SecureString Key { get; } - - /// - /// The private key's pass phrase - /// - public SecureString? Passphrase { get; } - - /// - /// Indicates if the private key is encrypted or not - /// - public bool IsEncrypted { get; } - - /// - /// Create a private key providing an encrypted key information - /// - /// The private key used for signing - /// The private key's passphrase - public PrivateKey(SecureString key, SecureString passphrase) - { - Key = key; - Passphrase = passphrase; - - IsEncrypted = true; - } - - /// - /// Create a private key providing an encrypted key information - /// - /// The private key used for signing - /// The private key's passphrase - 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; - } - - /// - /// Create a private key providing an unencrypted key information - /// - /// The private key used for signing - public PrivateKey(SecureString key) - { - Key = key; - - IsEncrypted = false; - } - - /// - /// Create a private key providing an encrypted key information - /// - /// The private key used for signing - public PrivateKey(string key) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Key can't be null/empty"); - - Key = key.ToSecureString(); - - IsEncrypted = false; - } - - /// - /// Copy the private key - /// - /// - public PrivateKey Copy() - { - if (Passphrase == null) - return new PrivateKey(Key.GetString()); - else - return new PrivateKey(Key.GetString(), Passphrase.GetString()); - } - - /// - /// Dispose - /// - public void Dispose() - { - Key?.Dispose(); - Passphrase?.Dispose(); - } - } -} diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 5407929..8014a25 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -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 /// /// Base API for all API clients /// - public abstract class BaseApiClient: IDisposable + public abstract class BaseApiClient : IDisposable, IBaseApiClient { private ApiCredentials? _apiCredentials; private AuthenticationProvider? _authenticationProvider; @@ -38,7 +39,7 @@ namespace CryptoExchange.Net /// public AuthenticationProvider? AuthenticationProvider { - get + get { if (!_created && !_disposing && _apiCredentials != null) { @@ -98,7 +99,7 @@ namespace CryptoExchange.Net /// /// Lock for id generating /// - protected static object idLock = new (); + protected static object idLock = new(); /// /// A default serializer @@ -131,7 +132,7 @@ namespace CryptoExchange.Net protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); /// - public void SetApiCredentials(ApiCredentials credentials) + public void SetApiCredentials(T credentials) where T : ApiCredentials { _apiCredentials = credentials?.Copy(); _created = false; diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index ec26ae0..db3f90c 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -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. /// /// The credentials to set - public void SetApiCredentials(ApiCredentials credentials) + protected virtual void SetApiCredentials(T credentials) where T : ApiCredentials { foreach (var apiClient in ApiClients) apiClient.SetApiCredentials(credentials); diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 6306b62..8c3a0c9 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -20,35 +20,24 @@ namespace CryptoExchange.Net /// /// Base rest API client for interacting with a REST API /// - public abstract class RestApiClient: BaseApiClient + public abstract class RestApiClient : BaseApiClient, IRestApiClient { - /// - /// The factory for creating requests. Used for unit testing - /// + /// public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); + /// + public abstract TimeSyncInfo? GetTimeSyncInfo(); + + /// + public abstract TimeSpan? GetTimeOffset(); + + /// + public int TotalRequestsMade { get; set; } /// /// Request headers to be sent with each request /// protected Dictionary? StandardRequestHeaders { get; set; } - /// - /// Get time sync info for an API client - /// - /// - public abstract TimeSyncInfo GetTimeSyncInfo(); - - /// - /// Get time offset for an API client - /// - /// - public abstract TimeSpan GetTimeOffset(); - - /// - /// Total amount of requests made with this API client - /// - public int TotalRequestsMade { get; set; } - /// /// Options for this client /// @@ -70,7 +59,7 @@ namespace CryptoExchange.Net /// Logger /// The base client options /// The Api client options - 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(); foreach (var rateLimiter in apiOptions.RateLimiters) @@ -110,12 +99,20 @@ namespace CryptoExchange.Net Dictionary? 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(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false); - return result.AsDataless(); + var result = await GetResponseAsync(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false); + if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false)) + continue; + + return result.AsDataless(); + } } /// @@ -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(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!); - return await GetResponseAsync(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false); + var result = await GetResponseAsync(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false); + if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false)) + continue; + + return result; + } } /// @@ -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(request); } - - /// /// Executes the request and returns the result deserialized into the type parameter class /// @@ -376,6 +381,16 @@ namespace CryptoExchange.Net return Task.FromResult(null); } + /// + /// 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 + /// + /// WebCallResult type parameter + /// The result of the call + /// The current try number + /// True if call should retry, false if the call should return + protected virtual Task ShouldRetryRequestAsync(WebCallResult callResult, int tries) => Task.FromResult(false); + /// /// Creates a request object /// @@ -521,11 +536,14 @@ namespace CryptoExchange.Net /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues /// /// Server time - protected abstract Task> GetServerTimestampAsync(); + protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); internal async Task> SyncTimeAsync() { var timeSyncParams = GetTimeSyncInfo(); + if (timeSyncParams == null) + return new WebCallResult(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)) @@ -557,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(null, null, null, null, null, null, null, null, true, null); diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 1032277..ec7df5a 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -18,13 +18,10 @@ namespace CryptoExchange.Net /// /// Base socket API client for interaction with a websocket API /// - public abstract class SocketApiClient : BaseApiClient + public abstract class SocketApiClient : BaseApiClient, ISocketApiClient { #region Fields - - /// - /// The factory for creating sockets. Used for unit testing - /// + /// public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory(); /// @@ -117,7 +114,7 @@ namespace CryptoExchange.Net /// log /// Client options /// The Api client options - 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; } diff --git a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs new file mode 100644 index 0000000..8daf1ef --- /dev/null +++ b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs @@ -0,0 +1,17 @@ +using CryptoExchange.Net.Authentication; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Base api client + /// + public interface IBaseApiClient + { + /// + /// Set the API credentials for this API client + /// + /// + /// + void SetApiCredentials(T credentials) where T : ApiCredentials; + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/IRestApiClient.cs b/CryptoExchange.Net/Interfaces/IRestApiClient.cs new file mode 100644 index 0000000..e6a63e1 --- /dev/null +++ b/CryptoExchange.Net/Interfaces/IRestApiClient.cs @@ -0,0 +1,33 @@ +using CryptoExchange.Net.Objects; +using System; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Base rest API client + /// + public interface IRestApiClient : IBaseApiClient + { + /// + /// The factory for creating requests. Used for unit testing + /// + IRequestFactory RequestFactory { get; set; } + + /// + /// Total amount of requests made with this API client + /// + int TotalRequestsMade { get; set; } + + /// + /// Get time offset for an API client. Return null if time syncing shouldnt/cant be done + /// + /// + TimeSpan? GetTimeOffset(); + + /// + /// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done + /// + /// + TimeSyncInfo? GetTimeSyncInfo(); + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index b5ad0ef..3255e38 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -18,11 +18,5 @@ namespace CryptoExchange.Net.Interfaces /// The total amount of requests made with this client /// int TotalRequestsMade { get; } - - /// - /// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options. - /// - /// The credentials to set - void SetApiCredentials(ApiCredentials credentials); } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs b/CryptoExchange.Net/Interfaces/ISocketApiClient.cs new file mode 100644 index 0000000..bd49157 --- /dev/null +++ b/CryptoExchange.Net/Interfaces/ISocketApiClient.cs @@ -0,0 +1,81 @@ +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Sockets; +using System; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Socket API client + /// + public interface ISocketApiClient: IBaseApiClient + { + /// + /// The current amount of socket connections on the API client + /// + int CurrentConnections { get; } + /// + /// The current amount of subscriptions over all connections + /// + int CurrentSubscriptions { get; } + /// + /// Incoming data kpbs + /// + double IncomingKbps { get; } + /// + /// Client options + /// + SocketApiClientOptions Options { get; } + /// + /// The factory for creating sockets. Used for unit testing + /// + IWebsocketFactory SocketFactory { get; set; } + + /// + /// Get the url to reconnect to after losing a connection + /// + /// + /// + Task GetReconnectUriAsync(SocketConnection connection); + + /// + /// Log the current state of connections and subscriptions + /// + string GetSubscriptionsState(); + /// + /// Reconnect all connections + /// + /// + Task ReconnectAsync(); + /// + /// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example. + /// + /// The original request + /// + Task> RevitalizeRequestAsync(object request); + /// + /// Periodically sends data over a socket connection + /// + /// Identifier for the periodic send + /// How often + /// Method returning the object to send + void SendPeriodic(string identifier, TimeSpan interval, Func objGetter); + /// + /// Unsubscribe all subscriptions + /// + /// + Task UnsubscribeAllAsync(); + /// + /// Unsubscribe an update subscription + /// + /// The id of the subscription to unsubscribe + /// + Task UnsubscribeAsync(int subscriptionId); + /// + /// Unsubscribe an update subscription + /// + /// The subscription to unsubscribe + /// + Task UnsubscribeAsync(UpdateSubscription subscription); + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 54d736e..c007101 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -16,12 +16,6 @@ namespace CryptoExchange.Net.Interfaces /// ClientOptions ClientOptions { get; } - /// - /// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options. - /// - /// The credentials to set - void SetApiCredentials(ApiCredentials credentials); - /// /// Incoming kilobytes per second of data ///