From 0be1bb16e3d48b136016ad66a1ccab6f02a6993f Mon Sep 17 00:00:00 2001 From: Jan Korf Date: Mon, 23 Dec 2024 08:49:58 +0100 Subject: [PATCH] Feature/update settings (#225) Added SetOptions method to update client settings Added SocketConnection parameter to PeriodicQuery callback Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI Added support for overriding request time out per request Changed max wait time for close handshake response from 5 seconds to 1 second Fixed exception in trade tracker when there is no data in the initial snapshot --- CryptoExchange.Net/Clients/BaseApiClient.cs | 11 ++++ CryptoExchange.Net/Clients/RestApiClient.cs | 8 +++ CryptoExchange.Net/Clients/SocketApiClient.cs | 24 +++++++- .../Interfaces/IBaseApiClient.cs | 8 +++ .../Interfaces/IRequestFactory.cs | 9 ++- CryptoExchange.Net/Interfaces/IWebsocket.cs | 5 ++ .../Objects/Options/UpdateOptions.cs | 29 ++++++++++ CryptoExchange.Net/Requests/RequestFactory.cs | 55 +++++++++++-------- .../Sockets/CryptoExchangeWebSocketClient.cs | 10 +++- .../Sockets/PeriodicTaskRegistration.cs | 2 +- CryptoExchange.Net/Sockets/Query.cs | 5 ++ .../Sockets/SocketConnection.cs | 18 +++++- .../Implementations/TestRequestFactory.cs | 2 + .../Testing/Implementations/TestSocket.cs | 2 + .../Trackers/Trades/TradeTracker.cs | 3 +- 15 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 CryptoExchange.Net/Objects/Options/UpdateOptions.cs diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index f47c8fd..8d1889b 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -93,6 +93,17 @@ namespace CryptoExchange.Net.Clients AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); } + /// + public virtual void SetOptions(UpdateOptions options) where T : ApiCredentials + { + ClientOptions.Proxy = options.Proxy; + ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout; + + ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials; + if (options.ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy()); + } + /// /// Dispose /// diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 41e22d1..91e999a 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -961,6 +961,14 @@ namespace CryptoExchange.Net.Clients /// Server time protected virtual Task> GetServerTimestampAsync() => throw new NotImplementedException(); + /// + public override void SetOptions(UpdateOptions options) + { + base.SetOptions(options); + + RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout); + } + internal async Task> SyncTimeAsync() { var timeSyncParams = GetTimeSyncInfo(); diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index d27ee3d..1adcba6 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -158,7 +158,7 @@ namespace CryptoExchange.Net.Clients /// /// /// - protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration { @@ -422,9 +422,10 @@ namespace CryptoExchange.Net.Clients result.Error!.Message = "Authentication failed: " + result.Error.Message; return new CallResult(result.Error)!; } + + _logger.Authenticated(socket.SocketId); } - _logger.Authenticated(socket.SocketId); socket.Authenticated = true; return new CallResult(null); } @@ -710,6 +711,25 @@ namespace CryptoExchange.Net.Clients return new CallResult(null); } + /// + public override void SetOptions(UpdateOptions options) + { + var previousProxyIsSet = ClientOptions.Proxy != null; + base.SetOptions(options); + + if ((!previousProxyIsSet && options.Proxy == null) + || !socketConnections.Any()) + { + return; + } + + _logger.LogInformation("Reconnecting websockets to apply proxy"); + + // Update proxy, also triggers reconnect + foreach (var connection in socketConnections) + _ = connection.Value.UpdateProxy(options.Proxy); + } + /// /// Log the current state of connections and subscriptions /// diff --git a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs index 548a196..9b321b1 100644 --- a/CryptoExchange.Net/Interfaces/IBaseApiClient.cs +++ b/CryptoExchange.Net/Interfaces/IBaseApiClient.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using System; @@ -31,5 +32,12 @@ namespace CryptoExchange.Net.Interfaces /// /// void SetApiCredentials(T credentials) where T : ApiCredentials; + + /// + /// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset. + /// + /// Api crentials type + /// Options to set + void SetOptions(UpdateOptions options) where T : ApiCredentials; } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index 66ff906..f3cc827 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces /// Request timeout to use /// Optional shared http client instance /// Optional proxy to use when no http client is provided - void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null); + void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null); + + /// + /// Update settings + /// + /// Proxy to use + /// Request timeout to use + void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout); } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index b1e75cb..721688d 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -93,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces /// /// Task CloseAsync(); + + /// + /// Update proxy setting + /// + void UpdateProxy(ApiProxy? proxy); } } diff --git a/CryptoExchange.Net/Objects/Options/UpdateOptions.cs b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs new file mode 100644 index 0000000..9fc9ed0 --- /dev/null +++ b/CryptoExchange.Net/Objects/Options/UpdateOptions.cs @@ -0,0 +1,29 @@ +using CryptoExchange.Net.Authentication; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects.Options +{ + /// + /// Options to update + /// + public class UpdateOptions where T : ApiCredentials + { + /// + /// Proxy setting. Note that if this is not provided any previously set proxy will be reset + /// + public ApiProxy? Proxy { get; set; } + /// + /// Api credentials + /// + public T? ApiCredentials { get; set; } + /// + /// Request timeout + /// + public TimeSpan? RequestTimeout { get; set; } + } + + /// + public class UpdateOptions : UpdateOptions { } +} diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index 83ea61d..693e91d 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -17,28 +17,7 @@ namespace CryptoExchange.Net.Requests public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) { if (client == null) - { - var handler = new HttpClientHandler(); - try - { - handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; - } - catch (PlatformNotSupportedException) { } - - if (proxy != null) - { - handler.Proxy = new WebProxy - { - Address = new Uri($"{proxy.Host}:{proxy.Port}"), - Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) - }; - } - - client = new HttpClient(handler) - { - Timeout = requestTimeout - }; - } + client = CreateClient(proxy, requestTimeout); _httpClient = client; } @@ -51,5 +30,37 @@ namespace CryptoExchange.Net.Requests return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); } + + /// + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) + { + _httpClient = CreateClient(proxy, requestTimeout); + } + + private HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) + { + var handler = new HttpClientHandler(); + try + { + handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials; + } + catch (PlatformNotSupportedException) { } + + if (proxy != null) + { + handler.Proxy = new WebProxy + { + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) + }; + } + + var client = new HttpClient(handler) + { + Timeout = requestTimeout + }; + return client; + } } } diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index fd3fb65..1d364eb 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -155,6 +155,12 @@ namespace CryptoExchange.Net.Sockets _baseAddress = $"{Uri.Scheme}://{Uri.Host}"; } + /// + public void UpdateProxy(ApiProxy? proxy) + { + Parameters.Proxy = proxy; + } + /// public virtual async Task ConnectAsync() { @@ -435,8 +441,8 @@ namespace CryptoExchange.Net.Sockets { // Wait until we receive close confirmation await Task.Delay(10).ConfigureAwait(false); - if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5)) - break; // Wait for max 5 seconds, then just abort the connection + if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1)) + break; // Wait for max 1 second, then just abort the connection } } } diff --git a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs index 8f9ccaf..9c532bb 100644 --- a/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs +++ b/CryptoExchange.Net/Sockets/PeriodicTaskRegistration.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets /// /// Callback after query /// - public Action? Callback { get; set; } + public Action? Callback { get; set; } } } diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 833abf8..397ae43 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets /// public bool Completed { get; set; } + /// + /// Timeout for the request + /// + public TimeSpan? RequestTimeout { get; set; } + /// /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// and each symbol receives it's own confirmation response diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 3e4364f..a1deaaa 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -11,6 +11,8 @@ using System.Diagnostics; using CryptoExchange.Net.Clients; using CryptoExchange.Net.Logging.Extensions; using System.Threading; +using CryptoExchange.Net.Objects.Options; +using CryptoExchange.Net.Authentication; namespace CryptoExchange.Net.Sockets { @@ -437,7 +439,7 @@ namespace CryptoExchange.Net.Sockets return Task.CompletedTask; } - query.IsSend(ApiClient.ClientOptions.RequestTimeout); + query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout); return Task.CompletedTask; } @@ -583,6 +585,16 @@ namespace CryptoExchange.Net.Sockets /// public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); + /// + /// Update the proxy setting and reconnect + /// + /// New proxy setting + public async Task UpdateProxy(ApiProxy? proxy) + { + _socket.UpdateProxy(proxy); + await TriggerReconnectAsync().ConfigureAwait(false); + } + /// /// Close the connection /// @@ -988,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets /// How often /// Method returning the query to send /// The callback for processing the response - public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) + public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func queryDelegate, Action? callback) { if (queryDelegate == null) throw new ArgumentNullException(nameof(queryDelegate)); @@ -1020,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets try { var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); - callback?.Invoke(result); + callback?.Invoke(this, result); } catch (Exception ex) { diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs b/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs index 6bcd8c1..293c2ea 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequestFactory.cs @@ -25,5 +25,7 @@ namespace CryptoExchange.Net.Testing.Implementations _request.RequestId = requestId; return _request; } + + public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {} } } diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs index b65cfa3..bec8b8d 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs @@ -92,5 +92,7 @@ namespace CryptoExchange.Net.Testing.Implementations public Task ReconnectAsync() => throw new NotImplementedException(); public void Dispose() { } + + public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException(); } } diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index 8962566..05bc209 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -350,7 +350,8 @@ namespace CryptoExchange.Net.Trackers.Trades _data.Add(item); } - _firstTimestamp = _data.Min(v => v.Timestamp); + if (_data.Any()) + _firstTimestamp = _data.Min(v => v.Timestamp); ApplyWindow(false); }