From 8b619e82f2953c88e15c1a52e3a09b8de495dfed Mon Sep 17 00:00:00 2001 From: Jkorf Date: Wed, 24 Nov 2021 16:39:14 +0100 Subject: [PATCH] Added DateTimeConverter as replacement for individual converters, fix for not closing socket when auth fails --- .../ConverterTests.cs | 58 ++++++++ .../Converters/DateTimeConverter.cs | 132 ++++++++++++++++++ .../TimestampMicroSecondsConverter.cs | 35 +++++ CryptoExchange.Net/Objects/Options.cs | 13 +- CryptoExchange.Net/SocketClient.cs | 1 + 5 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 CryptoExchange.Net.UnitTests/ConverterTests.cs create mode 100644 CryptoExchange.Net/Converters/DateTimeConverter.cs create mode 100644 CryptoExchange.Net/Converters/TimestampMicroSecondsConverter.cs diff --git a/CryptoExchange.Net.UnitTests/ConverterTests.cs b/CryptoExchange.Net.UnitTests/ConverterTests.cs new file mode 100644 index 0000000..83c7fab --- /dev/null +++ b/CryptoExchange.Net.UnitTests/ConverterTests.cs @@ -0,0 +1,58 @@ +using CryptoExchange.Net.Converters; +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture()] + public class ConverterTests + { + [TestCase("2021-05-12")] + [TestCase("20210512")] + [TestCase("210512")] + [TestCase("1620777600.000")] + [TestCase("1620777600000")] + [TestCase("2021-05-12T00:00:00.000Z")] + public void TestDateTimeConverterString(string input) + { + var output = JsonConvert.DeserializeObject($"{{ \"time\": \"{input}\" }}"); + Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [TestCase(1620777600.000)] + [TestCase(1620777600000d)] + public void TestDateTimeConverterDouble(double input) + { + var output = JsonConvert.DeserializeObject($"{{ \"time\": {input} }}"); + Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [TestCase(1620777600)] + [TestCase(1620777600000)] + [TestCase(1620777600000000)] + [TestCase(1620777600000000000)] + public void TestDateTimeConverterLong(long input) + { + var output = JsonConvert.DeserializeObject($"{{ \"time\": {input} }}"); + Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [Test] + public void TestDateTimeConverterNull() + { + var output = JsonConvert.DeserializeObject($"{{ \"time\": null }}"); + Assert.AreEqual(output.Time, null); + } + } + + public class TimeObject + { + [JsonConverter(typeof(DateTimeConverter))] + public DateTime? Time { get; set; } + } +} diff --git a/CryptoExchange.Net/Converters/DateTimeConverter.cs b/CryptoExchange.Net/Converters/DateTimeConverter.cs new file mode 100644 index 0000000..0e2b25d --- /dev/null +++ b/CryptoExchange.Net/Converters/DateTimeConverter.cs @@ -0,0 +1,132 @@ +using Newtonsoft.Json; +using System; +using System.Diagnostics; + +namespace CryptoExchange.Net.Converters +{ + /// + /// Datetime converter + /// + public class DateTimeConverter: JsonConverter + { + private static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; + private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; + + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(DateTime) || objectType == typeof(DateTime?); + } + + /// + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.Value == null) + return null; + + if(reader.TokenType is JsonToken.Integer) + { + var longValue = (long)reader.Value; + if (longValue < 1999999999) + return ConvertFromSeconds(longValue); + if (longValue < 1999999999999) + return ConvertFromMilliseconds(longValue); + if (longValue < 1999999999999999) + return ConvertFromMicroseconds(longValue); + + return ConvertFromNanoseconds(longValue); + } + else if (reader.TokenType is JsonToken.Float) + { + var doubleValue = (double)reader.Value; + if (doubleValue < 1999999999) + return ConvertFromSeconds(doubleValue); + + return ConvertFromMilliseconds(doubleValue); + } + else if(reader.TokenType is JsonToken.String) + { + var stringValue = (string)reader.Value; + if (stringValue.Length == 8) + { + // Parse 20211103 format + if (!int.TryParse(stringValue.Substring(0, 4), out var year) + || !int.TryParse(stringValue.Substring(4, 2), out var month) + || !int.TryParse(stringValue.Substring(6, 2), out var day)) + { + Debug.WriteLine("Unknown DateTime format: " + reader.Value); + return default; + } + return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + if (stringValue.Length == 6) + { + // Parse 211103 format + if (!int.TryParse(stringValue.Substring(0, 2), out var year) + || !int.TryParse(stringValue.Substring(2, 2), out var month) + || !int.TryParse(stringValue.Substring(4, 2), out var day)) + { + Debug.WriteLine("Unknown DateTime format: " + reader.Value); + return default; + } + return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + if (double.TryParse(stringValue, out var doubleValue)) + { + // Parse 1637745563.000 format + if (doubleValue < 1999999999) + return ConvertFromSeconds(doubleValue); + return ConvertFromMilliseconds(doubleValue); + } + + if(stringValue.Length == 10) + { + // Parse 2021-11-03 format + var values = stringValue.Split('-'); + if(!int.TryParse(values[0], out var year) + || !int.TryParse(values[1], out var month) + || !int.TryParse(values[2], out var day)) + { + Debug.WriteLine("Unknown DateTime format: " + reader.Value); + return default; + } + + return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + return JsonConvert.DeserializeObject(stringValue); + } + else if(reader.TokenType == JsonToken.Date) + { + return (DateTime)reader.Value; + } + else + { + Debug.WriteLine("Unknown DateTime format: " + reader.Value); + return default; + } + } + + public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddSeconds(seconds); + public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddMilliseconds(milliseconds); + public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * ticksPerMicrosecond)); + public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * ticksPerNanosecond)); + public static long ConvertToSeconds(DateTime time) => (long)Math.Round((time - _epoch).TotalSeconds); + public static long ConvertToMilliseconds(DateTime time) => (long)Math.Round((time - _epoch).TotalMilliseconds); + public static long ConvertToMicroseconds(DateTime time) => (long)Math.Round((time - _epoch).Ticks / ticksPerMicrosecond); + public static long ConvertToNanoseconds(DateTime time) => (long)Math.Round((time - _epoch).Ticks / ticksPerNanosecond); + + + /// + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) + writer.WriteValue((DateTime?)null); + else + writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).TotalMilliseconds)); + } + } +} diff --git a/CryptoExchange.Net/Converters/TimestampMicroSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampMicroSecondsConverter.cs new file mode 100644 index 0000000..033e25f --- /dev/null +++ b/CryptoExchange.Net/Converters/TimestampMicroSecondsConverter.cs @@ -0,0 +1,35 @@ +using System; +using Newtonsoft.Json; + +namespace CryptoExchange.Net.Converters +{ + /// + /// Converter for nanoseconds to datetime + /// + public class TimestampMicroSecondsConverter : JsonConverter + { + private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; + + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(DateTime); + } + + /// + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.Value == null) + return null; + + var nanoSeconds = long.Parse(reader.Value.ToString()); + return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerMicrosecond)); + } + + /// + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).Ticks / ticksPerMicrosecond)); + } + } +} diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 24c5f58..6d135d1 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -78,10 +78,15 @@ namespace CryptoExchange.Net.Objects if (value == null) return; - var newValue = value; - if (!newValue.EndsWith("/")) - newValue += "/"; - _baseAddress = newValue; + // TODO addresses can't always be forced to end with '/', bybit websocket doesn't work with it. + // Should be fixed in the GetUrl methods? + + //var newValue = value; + //if (!newValue.EndsWith("/")) + // newValue += "/"; + //_baseAddress = newValue; + + _baseAddress = value; } } diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index 0f4ad84..fc4c29f 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -346,6 +346,7 @@ namespace CryptoExchange.Net var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false); if (!result) { + await socket.CloseAsync().ConfigureAwait(false); log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} authentication failed"); result.Error!.Message = "Authentication failed: " + result.Error.Message; return new CallResult(false, result.Error);