diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 7460bb5..43ca4c9 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -26,10 +26,6 @@ namespace CryptoExchange.Net /// protected internal Log log; /// - /// The authentication provider when api credentials have been provided - /// - protected internal AuthenticationProvider? authProvider; - /// /// The last used id, use NextId() to get the next id and up this /// protected static int lastId; @@ -57,11 +53,9 @@ namespace CryptoExchange.Net /// /// The name of the exchange this client is for /// The options for this client - /// The authentication provider for this client (can be null if no credentials are provided) - protected BaseClient(string exchangeName, ClientOptions options, AuthenticationProvider? authenticationProvider) + protected BaseClient(string exchangeName, ClientOptions options) { log = new Log(exchangeName); - authProvider = authenticationProvider; log.UpdateWriters(options.LogWriters); log.Level = options.LogLevel; @@ -72,16 +66,6 @@ namespace CryptoExchange.Net log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}"); } - /// - /// Set the authentication provider, can be used when manually setting the API credentials - /// - /// - protected void SetAuthenticationProvider(AuthenticationProvider authenticationProvider) - { - log.Write(LogLevel.Debug, "Setting api credentials"); - authProvider = authenticationProvider; - } - /// /// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json /// @@ -273,32 +257,13 @@ namespace CryptoExchange.Net } } - /// - /// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence - /// - /// The total path string - /// The values to fill - /// - protected static string FillPathParameter(string path, params string[] values) - { - foreach (var value in values) - { - var index = path.IndexOf("{}", StringComparison.Ordinal); - if (index >= 0) - { - path = path.Remove(index, 2); - path = path.Insert(index, value); - } - } - return path; - } - /// /// Dispose /// public virtual void Dispose() { - authProvider?.Credentials?.Dispose(); + // TODO + //authProvider?.Credentials?.Dispose(); log.Write(LogLevel.Debug, "Disposing exchange client"); } } diff --git a/CryptoExchange.Net/Converters/DateTimeConverter.cs b/CryptoExchange.Net/Converters/DateTimeConverter.cs index f2edcb6..4d471e2 100644 --- a/CryptoExchange.Net/Converters/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/DateTimeConverter.cs @@ -85,7 +85,12 @@ namespace CryptoExchange.Net.Converters // Parse 1637745563.000 format if (doubleValue < 1999999999) return ConvertFromSeconds(doubleValue); - return ConvertFromMilliseconds(doubleValue); + if (doubleValue < 1999999999999) + return ConvertFromMilliseconds((long)doubleValue); + if (doubleValue < 1999999999999999) + return ConvertFromMicroseconds((long)doubleValue); + + return ConvertFromNanoseconds((long)doubleValue); } if(stringValue.Length == 10) diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 26e0418..55a265e 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -72,7 +72,7 @@ namespace CryptoExchange.Net /// public static void AddOptionalParameter(this Dictionary parameters, string key, object? value) { - if(value != null) + if (value != null) parameters.Add(key, value); } @@ -127,7 +127,7 @@ namespace CryptoExchange.Net var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); foreach (var arrayEntry in arraysParameters) { - if(serializationType == ArrayParametersSerialization.Array) + if (serializationType == ArrayParametersSerialization.Array) uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; else { @@ -369,6 +369,26 @@ namespace CryptoExchange.Net return url.TrimEnd('/'); } + + /// + /// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence + /// + /// The total path string + /// The values to fill + /// + public static string FillPathParameters(this string path, params string[] values) + { + foreach (var value in values) + { + var index = path.IndexOf("{}", StringComparison.Ordinal); + if (index >= 0) + { + path = path.Remove(index, 2); + path = path.Insert(index, value); + } + } + return path; + } } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index d16e33d..b828208 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -60,33 +60,42 @@ namespace CryptoExchange.Net.Objects public bool ChecksumValidationEnabled { get; set; } = true; } - /// - /// Base client options - /// - public class ClientOptions : BaseOptions + public class SubClientOptions { - private string _baseAddress = string.Empty; - /// - /// The base address of the client + /// The base address of the sub client /// - public string BaseAddress - { - get => _baseAddress; - set - { - if (value == null) - return; - - _baseAddress = value; - } - } + public string BaseAddress { get; set; } /// /// The api credentials used for signing requests /// public ApiCredentials? ApiCredentials { get; set; } + /// + /// Copy the values of the def to the input + /// + /// + /// + /// + public new void Copy(T input, T def) where T : SubClientOptions + { + input.BaseAddress = def.BaseAddress; + input.ApiCredentials = def.ApiCredentials?.Copy(); + } + + /// + public override string ToString() + { + return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}"; + } + } + + /// + /// Base client options + /// + public class ClientOptions : BaseOptions + { /// /// Proxy to use when connecting /// @@ -102,22 +111,17 @@ namespace CryptoExchange.Net.Objects { base.Copy(input, def); - input.BaseAddress = def.BaseAddress; - input.ApiCredentials = def.ApiCredentials?.Copy(); input.Proxy = def.Proxy; } /// public override string ToString() { - return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}"; + return $"{base.ToString()}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}"; } } - /// - /// Base for rest client options - /// - public class RestClientOptions : ClientOptions + public class RestSubClientOptions: SubClientOptions { /// /// List of rate limiters to use @@ -129,6 +133,32 @@ namespace CryptoExchange.Net.Objects /// public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; + /// + /// Copy the values of the def to the input + /// + /// + /// + /// + public new void Copy(T input, T def) where T : RestSubClientOptions + { + base.Copy(input, def); + + input.RateLimiters = def.RateLimiters.ToList(); + input.RateLimitingBehaviour = def.RateLimitingBehaviour; + } + + /// + public override string ToString() + { + return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}"; + } + } + + /// + /// Base for rest client options + /// + public class RestClientOptions : ClientOptions + { /// /// The time the server has to respond to a request before timing out /// @@ -150,18 +180,21 @@ namespace CryptoExchange.Net.Objects base.Copy(input, def); input.HttpClient = def.HttpClient; - input.RateLimiters = def.RateLimiters.ToList(); - input.RateLimitingBehaviour = def.RateLimitingBehaviour; input.RequestTimeout = def.RequestTimeout; } /// public override string ToString() { - return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-": "set")}"; + return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-": "set")}"; } } + public class SocketSubClientOptions: SubClientOptions + { + // TODO do we need this? + } + /// /// Base for socket client options /// diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 62cdc55..4b2d229 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -59,11 +59,6 @@ namespace CryptoExchange.Net /// protected string requestBodyEmptyContent = "{}"; - /// - /// List of rate limiters - /// - protected IEnumerable RateLimiters { get; } - /// public int TotalRequestsMade { get; private set; } @@ -82,8 +77,7 @@ namespace CryptoExchange.Net /// /// The name of the exchange this client is for /// The options for this client - /// The authentication provider for this client (can be null if no credentials are provided) - protected RestClient(string exchangeName, RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(exchangeName, exchangeOptions, authenticationProvider) + protected RestClient(string exchangeName, RestClientOptions exchangeOptions) : base(exchangeName, exchangeOptions) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); @@ -91,10 +85,6 @@ namespace CryptoExchange.Net ClientOptions = exchangeOptions; RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy, exchangeOptions.HttpClient); - var rateLimiters = new List(); - foreach (var rateLimiter in exchangeOptions.RateLimiters) - rateLimiters.Add(rateLimiter); - RateLimiters = rateLimiters; } /// @@ -114,30 +104,32 @@ namespace CryptoExchange.Net /// [return: NotNull] protected virtual async Task> SendRequestAsync( + RestSubClient subClient, Uri uri, HttpMethod method, - CancellationToken cancellationToken, + CancellationToken cancellationToken, Dictionary? parameters = null, bool signed = false, HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, int requestWeight = 1, JsonSerializer? deserializer = null, - Dictionary? additionalHeaders = null) where T : class + Dictionary? additionalHeaders = null + ) where T : class { var requestId = NextId(); log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri); - if (signed && authProvider == null) + if (signed && subClient.AuthenticationProvider == null) { log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); return new WebCallResult(null, null, null, new NoApiCredentialsError()); } var paramsPosition = parameterPosition ?? ParameterPositions[method]; - var request = ConstructRequest(uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders); - foreach (var limiter in RateLimiters) + var request = ConstructRequest(subClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders); + foreach (var limiter in subClient.RateLimiters) { - var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, ClientOptions.ApiCredentials?.Key, ClientOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false); + var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, subClient.Options.ApiCredentials?.Key, subClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false); if (!limitResult.Success) return new WebCallResult(null, null, null, limitResult.Error); } @@ -275,6 +267,7 @@ namespace CryptoExchange.Net /// Additional headers to send with the request /// protected virtual IRequest ConstructRequest( + SubClient subClient, Uri uri, HttpMethod method, Dictionary? parameters, @@ -287,8 +280,8 @@ namespace CryptoExchange.Net parameters ??= new Dictionary(); var uriString = uri.ToString(); - if (authProvider != null) - parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, parameterPosition, arraySerialization); + if (subClient.AuthenticationProvider != null) + parameters = subClient.AuthenticationProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, parameterPosition, arraySerialization); if (parameterPosition == HttpMethodParameterPosition.InUri && parameters?.Any() == true) uriString += "?" + parameters.CreateParamString(true, arraySerialization); @@ -298,8 +291,8 @@ namespace CryptoExchange.Net request.Accept = Constants.JsonContentHeader; var headers = new Dictionary(); - if (authProvider != null) - headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, parameterPosition, arraySerialization); + if (subClient.AuthenticationProvider != null) + headers = subClient.AuthenticationProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, parameterPosition, arraySerialization); foreach (var header in headers) request.AddHeader(header.Key, header.Value); diff --git a/CryptoExchange.Net/RestSubClient.cs b/CryptoExchange.Net/RestSubClient.cs new file mode 100644 index 0000000..f77a7d5 --- /dev/null +++ b/CryptoExchange.Net/RestSubClient.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Requests; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CryptoExchange.Net +{ + /// + /// Base rest client + /// + public abstract class RestSubClient: SubClient + { + internal RestSubClientOptions Options { get; } + + /// + /// List of rate limiters + /// + internal IEnumerable RateLimiters { get; } + + public RestSubClient(RestSubClientOptions options, AuthenticationProvider? authProvider): base(options,authProvider) + { + Options = options; + + var rateLimiters = new List(); + foreach (var rateLimiter in options.RateLimiters) + rateLimiters.Add(rateLimiter); + RateLimiters = rateLimiters; + } + + } +} diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index fc4c29f..3073a6f 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -104,8 +104,7 @@ namespace CryptoExchange.Net /// /// The name of the exchange this client is for /// The options for this client - /// The authentication provider for this client (can be null if no credentials are provided) - protected SocketClient(string exchangeName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeName, exchangeOptions, authenticationProvider) + protected SocketClient(string exchangeName, SocketClientOptions exchangeOptions): base(exchangeName, exchangeOptions) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); @@ -134,9 +133,9 @@ namespace CryptoExchange.Net /// The handler of update data /// Cancellation token for closing this subscription /// - protected virtual Task> SubscribeAsync(object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) + protected virtual Task> SubscribeAsync(SocketSubClient subClient, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) { - return SubscribeAsync(ClientOptions.BaseAddress, request, identifier, authenticated, dataHandler, ct); + return SubscribeAsync(subClient, subClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct); } /// @@ -150,7 +149,7 @@ namespace CryptoExchange.Net /// The handler of update data /// Cancellation token for closing this subscription /// - protected virtual async Task> SubscribeAsync(string url, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) + protected virtual async Task> SubscribeAsync(SocketSubClient subClient, string url, object? request, string? identifier, bool authenticated, Action> dataHandler, CancellationToken ct) { SocketConnection socketConnection; SocketSubscription subscription; @@ -169,7 +168,7 @@ namespace CryptoExchange.Net try { // Get a new or existing socket connection - socketConnection = GetSocketConnection(url, authenticated); + socketConnection = GetSocketConnection(subClient, url, authenticated); // Add a subscription on the socket connection subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler); @@ -254,9 +253,9 @@ namespace CryptoExchange.Net /// The request to send, will be serialized to json /// If the query is to an authenticated endpoint /// - protected virtual Task> QueryAsync(object request, bool authenticated) + protected virtual Task> QueryAsync(SocketSubClient subClient, object request, bool authenticated) { - return QueryAsync(ClientOptions.BaseAddress, request, authenticated); + return QueryAsync(subClient, subClient.Options.BaseAddress, request, authenticated); } /// @@ -267,14 +266,14 @@ namespace CryptoExchange.Net /// The request to send /// Whether the socket should be authenticated /// - protected virtual async Task> QueryAsync(string url, object request, bool authenticated) + protected virtual async Task> QueryAsync(SocketSubClient subClient, string url, object request, bool authenticated) { SocketConnection socketConnection; var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - socketConnection = GetSocketConnection(url, authenticated); + socketConnection = GetSocketConnection(subClient, url, authenticated); if (ClientOptions.SocketSubscriptionsCombineTarget == 1) { // Can release early when only a single sub per connection @@ -481,9 +480,10 @@ namespace CryptoExchange.Net /// The address the socket is for /// Whether the socket should be authenticated /// - protected virtual SocketConnection GetSocketConnection(string address, bool authenticated) + protected virtual SocketConnection GetSocketConnection(SocketSubClient subClient, string address, bool authenticated) { - var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/') + var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/') + && (s.Value.SubClient.GetType() == subClient.GetType()) && (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault(); var result = socketResult.Equals(default(KeyValuePair)) ? null : socketResult.Value; if (result != null) @@ -497,7 +497,7 @@ namespace CryptoExchange.Net // Create new socket var socket = CreateSocket(address); - var socketConnection = new SocketConnection(this, socket); + var socketConnection = new SocketConnection(this, subClient, socket); socketConnection.UnhandledMessage += HandleUnhandledMessage; foreach (var kvp in genericHandlers) { diff --git a/CryptoExchange.Net/SocketSubClient.cs b/CryptoExchange.Net/SocketSubClient.cs new file mode 100644 index 0000000..e90a814 --- /dev/null +++ b/CryptoExchange.Net/SocketSubClient.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Requests; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CryptoExchange.Net +{ + /// + /// Base rest client + /// + public abstract class SocketSubClient : SubClient + { + internal SocketSubClientOptions Options { get; } + + public SocketSubClient(SocketSubClientOptions options, AuthenticationProvider? authProvider): base(options,authProvider) + { + Options = options; + } + + } +} diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index e0166ff..188d60e 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -224,7 +224,7 @@ namespace CryptoExchange.Net.Sockets try { using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - await _socket.ConnectAsync(new Uri(Url), default).ConfigureAwait(false); + await _socket.ConnectAsync(new Uri(Url), tcs.Token).ConfigureAwait(false); Handle(openHandlers); } diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 2332554..530f7e1 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -77,6 +77,8 @@ namespace CryptoExchange.Net.Sockets /// public IWebsocket Socket { get; set; } + public SocketSubClient SubClient { get; set; } + /// /// If the socket should be reconnected upon closing /// @@ -97,6 +99,11 @@ namespace CryptoExchange.Net.Sockets /// public DateTime? DisconnectTime { get; set; } + /// + /// Tag for identificaion + /// + public string? Tag { get; set; } + /// /// If activity is paused /// @@ -130,10 +137,11 @@ namespace CryptoExchange.Net.Sockets /// /// The socket client /// The socket - public SocketConnection(SocketClient client, IWebsocket socket) + public SocketConnection(SocketClient client, SocketSubClient subClient, IWebsocket socket) { log = client.log; socketClient = client; + SubClient = subClient; pendingRequests = new List(); diff --git a/CryptoExchange.Net/SubClient.cs b/CryptoExchange.Net/SubClient.cs new file mode 100644 index 0000000..6d97a09 --- /dev/null +++ b/CryptoExchange.Net/SubClient.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Requests; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CryptoExchange.Net +{ + /// + /// Base rest client + /// + public abstract class SubClient + { + public AuthenticationProvider? AuthenticationProvider { get; } + protected string BaseAddress { get; } + + public SubClient(SubClientOptions options, AuthenticationProvider? authProvider) + { + AuthenticationProvider = authProvider; + BaseAddress = options.BaseAddress; + } + + } +}