diff --git a/CryptoExchange.Net.UnitTests/ConverterTests.cs b/CryptoExchange.Net.UnitTests/ConverterTests.cs index 1d4705f..a4f43f8 100644 --- a/CryptoExchange.Net.UnitTests/ConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/ConverterTests.cs @@ -140,10 +140,10 @@ namespace CryptoExchange.Net.UnitTests [TestCase("four", TestEnum.Four)] [TestCase("Four1", null)] [TestCase(null, null)] - public void TestEnumConverterNullableDeserializeTests(string? value, TestEnum? expected) + public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonConvert.DeserializeObject<EnumObject?>($"{{ \"Value\": {val} }}"); + var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}"); Assert.AreEqual(output.Value, expected); } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index adb591e..dc580f1 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Objects; @@ -33,14 +34,11 @@ namespace CryptoExchange.Net.UnitTests { } - public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization) + public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers) { - return base.AddAuthenticationToHeaders(uri, method, parameters, signed, postParameters, arraySerialization); - } - - public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization) - { - return base.AddAuthenticationToParameters(uri, method, parameters, signed, postParameters, arraySerialization); + bodyParameters = new SortedDictionary<string, object>(); + uriParameters = new SortedDictionary<string, object>(); + headers = new Dictionary<string, string>(); } public override string Sign(string toSign) diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 535702a..e425830 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -56,10 +56,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.GetHeaders()).Returns(() => headers); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>())) - .Callback<HttpMethod, string, int>((method, uri, id) => + factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) + .Callback<HttpMethod, Uri, int>((method, uri, id) => { - request.Setup(a => a.Uri).Returns(new Uri(uri)); + request.Setup(a => a.Uri).Returns(uri); request.Setup(a => a.Method).Returns(method); }) .Returns(request.Object); @@ -76,7 +76,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>())) + factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) .Returns(request.Object); } @@ -99,8 +99,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations request.Setup(c => c.GetHeaders()).Returns(headers); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>())) - .Callback<HttpMethod, string, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(new Uri(uri))) + factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) + .Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Returns(request.Object); } @@ -122,8 +122,23 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } + public override TimeSpan GetTimeOffset() + { + throw new NotImplementedException(); + } + protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => new TestAuthProvider(credentials); + + protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() + { + throw new NotImplementedException(); + } + + protected override TimeSyncInfo GetTimeSyncInfo() + { + throw new NotImplementedException(); + } } public class TestRestApi2Client : RestApiClient @@ -133,8 +148,23 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } + public override TimeSpan GetTimeOffset() + { + throw new NotImplementedException(); + } + protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => new TestAuthProvider(credentials); + + protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() + { + throw new NotImplementedException(); + } + + protected override TimeSyncInfo GetTimeSyncInfo() + { + throw new NotImplementedException(); + } } public class TestAuthProvider : AuthenticationProvider @@ -142,6 +172,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public TestAuthProvider(ApiCredentials credentials) : base(credentials) { } + + public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers) + { + uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>(); + bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>(); + headers = new Dictionary<string, string>(); + } } public class ParseErrorTestRestClient: TestRestClient diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index eaf4d4a..2942d9e 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,6 +1,8 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; +using System.Globalization; using System.Net.Http; using System.Security.Cryptography; using System.Text; @@ -35,43 +37,41 @@ namespace CryptoExchange.Net.Authentication } /// <summary> - /// Authenticate a request where the parameters need to be in the Uri + /// Authenticate a request. Output parameters should include the providedParameters input /// </summary> /// <param name="apiClient">The Api client sending the request</param> /// <param name="uri">The uri for the request</param> /// <param name="method">The method of the request</param> - /// <param name="parameters">The request parameters</param> - /// <param name="headers">The request headers</param> + /// <param name="providedParameters">The request parameters</param> /// <param name="auth">If the requests should be authenticated</param> /// <param name="arraySerialization">Array serialization type</param> - /// <returns></returns> - public abstract void AuthenticateUriRequest( + /// <param name="parameterPosition">The position where the providedParameters should go</param> + /// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param> + /// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param> + /// <param name="headers">The headers that should be send with the request</param> + public abstract void AuthenticateRequest( RestApiClient apiClient, Uri uri, HttpMethod method, - SortedDictionary<string, object> parameters, - Dictionary<string, string> headers, + Dictionary<string, object> providedParameters, bool auth, - ArrayParametersSerialization arraySerialization); + ArrayParametersSerialization arraySerialization, + HttpMethodParameterPosition parameterPosition, + out SortedDictionary<string, object> uriParameters, + out SortedDictionary<string, object> bodyParameters, + out Dictionary<string, string> headers + ); /// <summary> - /// Authenticate a request where the parameters need to be in the request body + /// SHA256 sign the data and return the bytes /// </summary> - /// <param name="apiClient">The Api client sending the request</param> - /// <param name="uri">The uri for the request</param> - /// <param name="method">The method of the request</param> - /// <param name="parameters">The request parameters</param> - /// <param name="headers">The request headers</param> - /// <param name="auth">If the requests should be authenticated</param> - /// <param name="arraySerialization">Array serialization type</param> - public abstract void AuthenticateBodyRequest( - RestApiClient apiClient, - Uri uri, - HttpMethod method, - SortedDictionary<string, object> parameters, - Dictionary<string, string> headers, - bool auth, - ArrayParametersSerialization arraySerialization); + /// <param name="data"></param> + /// <returns></returns> + protected static byte[] SignSHA256Bytes(string data) + { + using var encryptor = SHA256.Create(); + return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + } /// <summary> /// SHA256 sign the data and return the hash @@ -158,9 +158,18 @@ namespace CryptoExchange.Net.Authentication /// <param name="outputType">String type</param> /// <returns></returns> protected string SignHMACSHA512(string data, SignOutputType? outputType = null) + => SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType); + + /// <summary> + /// HMACSHA512 sign the data and return the hash + /// </summary> + /// <param name="data">Data to sign</param> + /// <param name="outputType">String type</param> + /// <returns></returns> + protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null) { using var encryptor = new HMACSHA512(_sBytes); - var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); + var resultBytes = encryptor.ComputeHash(data); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); } @@ -206,5 +215,25 @@ namespace CryptoExchange.Net.Authentication { return Convert.ToBase64String(buff); } + + /// <summary> + /// Get current timestamp including the time sync offset from the api client + /// </summary> + /// <param name="apiClient"></param> + /// <returns></returns> + protected static DateTime GetTimestamp(RestApiClient apiClient) + { + return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!; + } + + /// <summary> + /// Get millisecond timestamp as a string including the time sync offset from the api client + /// </summary> + /// <param name="apiClient"></param> + /// <returns></returns> + protected static string GetMillisecondTimestamp(RestApiClient apiClient) + { + return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); + } } } diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index f7c6efd..1d21c3a 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -119,10 +119,13 @@ namespace CryptoExchange.Net { var requestId = NextId(); - var syncTimeResult = await apiClient.SyncTimeAsync().ConfigureAwait(false); - if (!syncTimeResult) - return syncTimeResult.As<T>(default); - + if (signed) + { + var syncTimeResult = await apiClient.SyncTimeAsync().ConfigureAwait(false); + if (!syncTimeResult) + return syncTimeResult.As<T>(default); + } + log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri); if (signed && apiClient.AuthenticationProvider == null) { @@ -285,33 +288,40 @@ namespace CryptoExchange.Net int requestId, Dictionary<string, string>? additionalHeaders) { - SortedDictionary<string, object> sortedParameters = new SortedDictionary<string, object>(GetParameterComparer()); - if (parameters != null) - sortedParameters = new SortedDictionary<string, object>(parameters, GetParameterComparer()); - + parameters ??= new Dictionary<string, object>(); if (parameterPosition == HttpMethodParameterPosition.InUri) { - foreach (var parameter in sortedParameters) + foreach (var parameter in parameters) uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()); } - var length = sortedParameters.Count; var headers = new Dictionary<string, string>(); + var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>(); + var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>(); if (apiClient.AuthenticationProvider != null) + apiClient.AuthenticationProvider.AuthenticateRequest( + apiClient, + uri, + method, + parameters, + signed, + arraySerialization, + parameterPosition, + out uriParameters, + out bodyParameters, + out headers); + + // Sanity check + foreach(var param in parameters) { - if(parameterPosition == HttpMethodParameterPosition.InUri) - apiClient.AuthenticationProvider.AuthenticateUriRequest(apiClient, uri, method, sortedParameters, headers, signed, arraySerialization); - else - apiClient.AuthenticationProvider.AuthenticateBodyRequest(apiClient, uri, method, sortedParameters, headers, signed, arraySerialization); - } - - if (parameterPosition == HttpMethodParameterPosition.InUri) - { - // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters - if (sortedParameters.Count != length) - uri = uri.SetParameters(sortedParameters); + if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key)) + throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " + + $"should return provided parameters in either the uri or body parameters output"); } + // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters + uri = uri.SetParameters(uriParameters); + var request = RequestFactory.Create(method, uri, requestId); request.Accept = Constants.JsonContentHeader; @@ -335,8 +345,8 @@ namespace CryptoExchange.Net if (parameterPosition == HttpMethodParameterPosition.InBody) { var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; - if (sortedParameters?.Any() == true) - WriteParamBody(request, sortedParameters, contentType); + if (bodyParameters.Any()) + WriteParamBody(request, bodyParameters, contentType); else request.SetContent(requestBodyEmptyContent, contentType); } @@ -386,8 +396,6 @@ namespace CryptoExchange.Net //return request; } - protected virtual IComparer<string> GetParameterComparer() => null; - /// <summary> /// Writes the parameters of the request to the request object body /// </summary> diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 1247425..00ef774 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -14,8 +14,16 @@ namespace CryptoExchange.Net /// </summary> public abstract class RestApiClient: BaseApiClient { - protected abstract TimeSyncModel GetTimeSyncParameters(); - protected abstract void UpdateTimeOffset(TimeSpan offset); + /// <summary> + /// Get time sync info for an API client + /// </summary> + /// <returns></returns> + protected abstract TimeSyncInfo GetTimeSyncInfo(); + + /// <summary> + /// Get time offset for an API client + /// </summary> + /// <returns></returns> public abstract TimeSpan GetTimeOffset(); /// <summary> @@ -33,8 +41,6 @@ namespace CryptoExchange.Net /// </summary> internal IEnumerable<IRateLimiter> RateLimiters { get; } - private Log _log; - /// <summary> /// ctor /// </summary> @@ -58,12 +64,12 @@ namespace CryptoExchange.Net internal async Task<WebCallResult<bool>> SyncTimeAsync() { - var timeSyncParams = GetTimeSyncParameters(); - if (await timeSyncParams.Semaphore.WaitAsync(0).ConfigureAwait(false)) + var timeSyncParams = GetTimeSyncInfo(); + if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) { - if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.LastSyncTime < TimeSpan.FromHours(1))) + if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < TimeSpan.FromHours(1))) { - timeSyncParams.Semaphore.Release(); + timeSyncParams.TimeSyncState.Semaphore.Release(); return new WebCallResult<bool>(null, null, true, null); } @@ -71,7 +77,7 @@ namespace CryptoExchange.Net var result = await GetServerTimestampAsync().ConfigureAwait(false); if (!result) { - timeSyncParams.Semaphore.Release(); + timeSyncParams.TimeSyncState.Semaphore.Release(); return result.As(false); } @@ -82,7 +88,7 @@ namespace CryptoExchange.Net result = await GetServerTimestampAsync().ConfigureAwait(false); if (!result) { - timeSyncParams.Semaphore.Release(); + timeSyncParams.TimeSyncState.Semaphore.Release(); return result.As(false); } } @@ -92,13 +98,13 @@ namespace CryptoExchange.Net if (offset.TotalMilliseconds >= 0 && offset.TotalMilliseconds < 500) { // Small offset, probably mainly due to ping. Don't adjust time - UpdateTimeOffset(offset); - timeSyncParams.Semaphore.Release(); + timeSyncParams.UpdateTimeOffset(offset); + timeSyncParams.TimeSyncState.Semaphore.Release(); } else { - UpdateTimeOffset(offset); - timeSyncParams.Semaphore.Release(); + timeSyncParams.UpdateTimeOffset(offset); + timeSyncParams.TimeSyncState.Semaphore.Release(); } } diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index a587352..b5fca32 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> </PropertyGroup> @@ -41,7 +41,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3"> + <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 1cf4ab0..de4a859 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -151,7 +151,7 @@ namespace CryptoExchange.Net public static string ToFormData(this SortedDictionary<string, object> parameters) { var formData = HttpUtility.ParseQueryString(string.Empty); - foreach (var kvp in parameters.OrderBy(p => p.Key)) + foreach (var kvp in parameters) { if (kvp.Value.GetType().IsArray) { @@ -433,6 +433,25 @@ namespace CryptoExchange.Net return uriBuilder.Uri; } + /// <summary> + /// Create a new uri with the provided parameters as query + /// </summary> + /// <param name="parameters"></param> + /// <param name="baseUri"></param> + /// <returns></returns> + public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters) + { + var uriBuilder = new UriBuilder(); + uriBuilder.Scheme = baseUri.Scheme; + uriBuilder.Host = baseUri.Host; + uriBuilder.Path = baseUri.AbsolutePath; + var httpValueCollection = HttpUtility.ParseQueryString(string.Empty); + foreach (var parameter in parameters) + httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); + uriBuilder.Query = httpValueCollection.ToString(); + return uriBuilder.Uri; + } + /// <summary> /// Add parameter to URI diff --git a/CryptoExchange.Net/Objects/TimeSyncModel.cs b/CryptoExchange.Net/Objects/TimeSyncModel.cs deleted file mode 100644 index 6bc1c64..0000000 --- a/CryptoExchange.Net/Objects/TimeSyncModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; - -namespace CryptoExchange.Net.Objects -{ - public class TimeSyncModel - { - public bool SyncTime { get; set; } - public SemaphoreSlim Semaphore { get; set; } - public DateTime LastSyncTime { get; set; } - - public TimeSyncModel(bool syncTime, SemaphoreSlim semaphore, DateTime lastSyncTime) - { - SyncTime = syncTime; - Semaphore = semaphore; - LastSyncTime = lastSyncTime; - } - } -} diff --git a/CryptoExchange.Net/Objects/TimeSyncState.cs b/CryptoExchange.Net/Objects/TimeSyncState.cs new file mode 100644 index 0000000..81f453b --- /dev/null +++ b/CryptoExchange.Net/Objects/TimeSyncState.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading; +using CryptoExchange.Net.Logging; +using Microsoft.Extensions.Logging; + +namespace CryptoExchange.Net.Objects +{ + /// <summary> + /// The time synchronization state of an API client + /// </summary> + public class TimeSyncState + { + /// <summary> + /// Semaphore to use for checking the time syncing. Should be shared instance among the API client + /// </summary> + public SemaphoreSlim Semaphore { get; } + /// <summary> + /// Last sync time for the API client + /// </summary> + public DateTime LastSyncTime { get; set; } + /// <summary> + /// Time offset for the API client + /// </summary> + public TimeSpan TimeOffset { get; set; } + + /// <summary> + /// ctor + /// </summary> + public TimeSyncState() + { + Semaphore = new SemaphoreSlim(1, 1); + } + } + + /// <summary> + /// Time synchronization info + /// </summary> + public class TimeSyncInfo + { + /// <summary> + /// Logger + /// </summary> + public Log Log { get; } + /// <summary> + /// Should synchronize time + /// </summary> + public bool SyncTime { get; } + /// <summary> + /// Time sync state for the API client + /// </summary> + public TimeSyncState TimeSyncState { get; } + + /// <summary> + /// ctor + /// </summary> + /// <param name="log"></param> + /// <param name="syncTime"></param> + /// <param name="syncState"></param> + public TimeSyncInfo(Log log, bool syncTime, TimeSyncState syncState) + { + Log = log; + SyncTime = syncTime; + TimeSyncState = syncState; + } + + /// <summary> + /// Set the time offset + /// </summary> + /// <param name="offset"></param> + public void UpdateTimeOffset(TimeSpan offset) + { + TimeSyncState.LastSyncTime = DateTime.UtcNow; + if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) + return; + + Log.Write(LogLevel.Information, $"Time offset set to {Math.Round(offset.TotalMilliseconds)}ms"); + TimeSyncState.TimeOffset = offset; + } + } +}