diff --git a/.travis.yml b/.travis.yml index 100393f..e2b1f4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ language: csharp mono: none solution: CryptoExchange.Net.sln -dotnet: 2.0.0 -dist: xenial +dotnet: 3.0 script: - - dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.0" + - dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.1" - dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index bf43577..c109479 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -23,7 +23,7 @@ namespace CryptoExchange.Net.UnitTests // arrange // act // assert - Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions() { ApiCredentials = new ApiCredentials(key, secret) })); + Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) })); } [TestCase] @@ -31,7 +31,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var stringBuilder = new StringBuilder(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new RestClientOptions("") { LogWriters = new List { new StringWriter(stringBuilder) } }); @@ -67,7 +67,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange var stringBuilder = new StringBuilder(); - var client = new TestBaseClient(new RestClientOptions() + var client = new TestBaseClient(new RestClientOptions("") { LogWriters = new List { new StringWriter(stringBuilder) }, LogVerbosity = verbosity diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj index f521b29..ed4890c 100644 --- a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp3.0 false diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 78fa402..d07c5d9 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -7,9 +7,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; -using System.Text; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.RateLimiter; @@ -105,11 +103,12 @@ namespace CryptoExchange.Net.UnitTests { // arrange // act - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { BaseAddress = "http://test.address.com", RateLimiters = new List{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))}, - RateLimitingBehaviour = RateLimitingBehaviour.Fail + RateLimitingBehaviour = RateLimitingBehaviour.Fail, + RequestTimeout = TimeSpan.FromMinutes(1) }); @@ -117,13 +116,14 @@ namespace CryptoExchange.Net.UnitTests Assert.IsTrue(client.BaseAddress == "http://test.address.com"); Assert.IsTrue(client.RateLimiters.Count() == 1); Assert.IsTrue(client.RateLimitBehaviour == RateLimitingBehaviour.Fail); + Assert.IsTrue(client.RequestTimeout == TimeSpan.FromMinutes(1)); } [TestCase] public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Fail @@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait @@ -171,7 +171,7 @@ namespace CryptoExchange.Net.UnitTests public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey() { // arrange - var client = new TestRestClient(new RestClientOptions() + var client = new TestRestClient(new RestClientOptions("") { RateLimiters = new List { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait, diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index 42c7ae1..cef45f4 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; @@ -19,7 +17,7 @@ namespace CryptoExchange.Net.UnitTests { //arrange //act - var client = new TestSocketClient(new SocketClientOptions() + var client = new TestSocketClient(new SocketClientOptions("") { BaseAddress = "http://test.address.com", ReconnectInterval = TimeSpan.FromSeconds(6) @@ -51,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests public void SocketMessages_Should_BeProcessedInDataHandlers() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -59,11 +57,11 @@ namespace CryptoExchange.Net.UnitTests var sub = new SocketConnection(client, socket); var rstEvent = new ManualResetEvent(false); JToken result = null; - sub.AddHandler("TestHandler", true, (connection, data) => + sub.AddHandler(SocketSubscription.CreateForIdentifier("TestHandler", true, (connection, data) => { result = data; rstEvent.Set(); - }); + })); client.ConnectSocketSub(sub); // act @@ -79,7 +77,7 @@ namespace CryptoExchange.Net.UnitTests { // arrange bool reconnected = false; - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -106,12 +104,12 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingStream_Should_CloseTheSocket() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.CanConnect = true; var sub = new SocketConnection(client, socket); client.ConnectSocketSub(sub); - var ups = new UpdateSubscription(sub, new SocketSubscription("Test", null, true, (d, a) => {})); + var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (d, a) => {})); // act client.Unsubscribe(ups).Wait(); @@ -124,7 +122,7 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingAll_Should_CloseAllSockets() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket1 = client.CreateSocket(); var socket2 = client.CreateSocket(); socket1.CanConnect = true; @@ -146,7 +144,7 @@ namespace CryptoExchange.Net.UnitTests public void FailingToConnectSocket_Should_ReturnError() { // arrange - var client = new TestSocketClient(new SocketClientOptions() { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); var socket = client.CreateSocket(); socket.CanConnect = false; var sub = new SocketConnection(client, socket); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index dc0cd25..9b36f42 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.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; @@ -7,7 +8,7 @@ namespace CryptoExchange.Net.UnitTests { public class TestBaseClient: BaseClient { - public TestBaseClient(): base(new RestClientOptions(), null) + public TestBaseClient(): base(new RestClientOptions("http://testurl.url"), null) { } @@ -37,12 +38,12 @@ namespace CryptoExchange.Net.UnitTests { } - public override Dictionary AddAuthenticationToHeaders(string uri, string method, Dictionary parameters, bool signed) + public override Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed) { return base.AddAuthenticationToHeaders(uri, method, parameters, signed); } - public override Dictionary AddAuthenticationToParameters(string uri, string method, Dictionary parameters, bool signed) + public override Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed) { return base.AddAuthenticationToParameters(uri, method, parameters, signed); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs index b856d0f..9e95043 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestHelpers.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Text; namespace CryptoExchange.Net.UnitTests.TestImplementations { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs index 7a6f2cc..0ad099d 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs @@ -1,7 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.UnitTests.TestImplementations { diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index e8bf674..870c833 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -3,12 +3,12 @@ using CryptoExchange.Net.Objects; using Moq; using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestRestClient: RestClient { - public TestRestClient() : base(new RestClientOptions(), null) + public TestRestClient() : base(new RestClientOptions("http://testurl.url"), null) { RequestFactory = new Mock().Object; } @@ -39,36 +39,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations responseStream.Seek(0, SeekOrigin.Begin); var response = new Mock(); - response.Setup(c => c.GetResponseStream()).Returns(responseStream); - + response.Setup(c => c.IsSuccessStatusCode).Returns(true); + response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetRequestStream()).Returns(Task.FromResult(requestStream)); - request.Setup(c => c.GetResponse()).Returns(Task.FromResult(response.Object)); + request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } public void SetErrorWithoutResponse(HttpStatusCode code, string message) { - var we = new WebException(); - var r = new HttpWebResponse(); - var re = new HttpResponseMessage(); - - typeof(HttpResponseMessage).GetField("_statusCode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(re, code); - typeof(HttpWebResponse).GetField("_httpResponseMessage", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(r, re); - typeof(WebException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); - typeof(WebException).GetField("_response", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, r); - + var we = new HttpRequestException(); + typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); - request.Setup(c => c.GetResponse()).Throws(we); + request.Setup(c => c.GetResponse(It.IsAny())).Throws(we); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } @@ -79,22 +71,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations responseStream.Write(expectedBytes, 0, expectedBytes.Length); responseStream.Seek(0, SeekOrigin.Begin); - var r = new Mock(); - r.Setup(x => x.GetResponseStream()).Returns(responseStream); - var we = new WebException("", null, WebExceptionStatus.Success, r.Object); - + var response = new Mock(); + response.Setup(c => c.IsSuccessStatusCode).Returns(false); + response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + var request = new Mock(); - request.Setup(c => c.Headers).Returns(new WebHeaderCollection()); - request.Setup(c => c.GetResponse()).Throws(we); + request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); + request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); - factory.Setup(c => c.Create(It.IsAny())) + factory.Setup(c => c.Create(It.IsAny(), It.IsAny())) .Returns(request.Object); } - public async Task> Request(string method = "GET") where T:class + public async Task> Request(CancellationToken ct = default) where T:class { - return await ExecuteRequest(new Uri("http://www.test.com"), method); + return await SendRequest(new Uri("http://www.test.com"), HttpMethod.Get, ct); } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs index 5430a71..ef757e9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Security.Authentication; -using System.Text; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; using WebSocket4Net; diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index d01dc94..43f18b9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -11,7 +11,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestSocketClient: SocketClient { - public TestSocketClient() : this(new SocketClientOptions()) + public TestSocketClient() : this(new SocketClientOptions("http://testurl.url")) { } @@ -32,32 +32,33 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations return ConnectSocket(sub).Result; } - protected override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) + protected internal override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) { throw new NotImplementedException(); } - protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult callResult) + protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, + out CallResult callResult) { throw new NotImplementedException(); } - protected override bool MessageMatchesHandler(JToken message, object request) + protected internal override bool MessageMatchesHandler(JToken message, object request) { throw new NotImplementedException(); } - protected override bool MessageMatchesHandler(JToken message, string identifier) + protected internal override bool MessageMatchesHandler(JToken message, string identifier) { return true; } - protected override Task> AuthenticateSocket(SocketConnection s) + protected internal override Task> AuthenticateSocket(SocketConnection s) { throw new NotImplementedException(); } - protected override Task Unsubscribe(SocketConnection connection, SocketSubscription s) + protected internal override Task Unsubscribe(SocketConnection connection, SocketSubscription s) { throw new NotImplementedException(); } diff --git a/CryptoExchange.Net/AssemblyInfo.cs b/CryptoExchange.Net/AssemblyInfo.cs new file mode 100644 index 0000000..987163f --- /dev/null +++ b/CryptoExchange.Net/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")] \ No newline at end of file diff --git a/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs new file mode 100644 index 0000000..67c9021 --- /dev/null +++ b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace CryptoExchange.Net.Attributes +{ + /// + /// Used for conversion in ArrayConverter + /// + public class JsonConversionAttribute: Attribute + { + } +} diff --git a/CryptoExchange.Net/Attributes/NullableAttributes.cs b/CryptoExchange.Net/Attributes/NullableAttributes.cs new file mode 100644 index 0000000..11267ea --- /dev/null +++ b/CryptoExchange.Net/Attributes/NullableAttributes.cs @@ -0,0 +1,210 @@ +#if !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + /// + /// Specifies that is allowed as an input even if the + /// corresponding type disallows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class AllowNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public AllowNullAttribute() { } + } + + /// + /// Specifies that is disallowed as an input even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, + Inherited = false + )] + + [ExcludeFromCodeCoverage] + internal sealed class DisallowNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DisallowNullAttribute() { } + } + + /// + /// Specifies that a method that will never return under any circumstance. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class DoesNotReturnAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public DoesNotReturnAttribute() { } + } + + /// + /// Specifies that the method will not return if the associated + /// parameter is passed the specified value. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// + /// Gets the condition parameter value. + /// Code after the method is considered unreachable by diagnostics if the argument + /// to the associated parameter matches this value. + /// + public bool ParameterValue { get; } + + /// + /// Initializes a new instance of the + /// class with the specified parameter value. + /// + /// + /// The condition parameter value. + /// Code after the method is considered unreachable by diagnostics if the argument + /// to the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) + { + ParameterValue = parameterValue; + } + } + + /// + /// Specifies that an output may be even if the + /// corresponding type disallows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class MaybeNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public MaybeNullAttribute() { } + } + + /// + /// Specifies that when a method returns , + /// the parameter may be even if the corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter may be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter may be . + /// + public MaybeNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + } + + /// + /// Specifies that an output is not even if the + /// corresponding type allows it. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class NotNullAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + public NotNullAttribute() { } + } + + /// + /// Specifies that the output will be non- if the + /// named parameter is non-. + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + AllowMultiple = true, + Inherited = false + )] + [ExcludeFromCodeCoverage] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// + /// Gets the associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public string ParameterName { get; } + + /// + /// Initializes the attribute with the associated parameter name. + /// + /// + /// The associated parameter name. + /// The output will be non- if the argument to the + /// parameter specified is non-. + /// + public NotNullIfNotNullAttribute(string parameterName) + { + // .NET Core 3.0 doesn't throw an ArgumentNullException, even though this is + // tagged as non-null. + // Follow this behavior here. + ParameterName = parameterName; + } + } + + /// + /// Specifies that when a method returns , + /// the parameter will not be even if the corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage] + internal sealed class NotNullWhenAttribute : Attribute + { + /// + /// Gets the return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public bool ReturnValue { get; } + + /// + /// Initializes the attribute with the specified return value condition. + /// + /// + /// The return value condition. + /// If the method returns this value, the associated parameter will not be . + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + } +} +#endif \ No newline at end of file diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 6b2d56a..f863fcd 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -14,17 +14,17 @@ namespace CryptoExchange.Net.Authentication /// /// The api key to authenticate requests /// - public SecureString Key { get; private set; } + public SecureString? Key { get; } /// /// The api secret to authenticate requests /// - public SecureString Secret { get; private set; } + public SecureString? Secret { get; } /// /// The private key to authenticate requests /// - public PrivateKey PrivateKey { get; } + public PrivateKey? PrivateKey { get; } /// /// Create Api credentials providing a private key for authentication @@ -56,8 +56,20 @@ namespace CryptoExchange.Net.Authentication if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) throw new ArgumentException("Key and secret can't be null/empty"); - Key = CreateSecureString(key); - Secret = CreateSecureString(secret); + Key = key.ToSecureString(); + Secret = secret.ToSecureString(); + } + + /// + /// Copy the credentials + /// + /// + public ApiCredentials Copy() + { + if (PrivateKey == null) + return new ApiCredentials(Key!.GetString(), Secret!.GetString()); + else + return new ApiCredentials(PrivateKey!.Copy()); } /// @@ -66,24 +78,23 @@ namespace CryptoExchange.Net.Authentication /// The stream containing the json data /// A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'. /// A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'. - public ApiCredentials(Stream inputStream, string identifierKey = null, string identifierSecret = null) + public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) { - using (var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true)) - { - var stringData = reader.ReadToEnd(); - var jsonData = stringData.ToJToken(); - if(jsonData == null) - throw new ArgumentException("Input stream not valid json data"); + using var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true); + + var stringData = reader.ReadToEnd(); + var jsonData = stringData.ToJToken(); + if(jsonData == null) + throw new ArgumentException("Input stream not valid json data"); - var key = TryGetValue(jsonData, identifierKey ?? "apiKey"); - var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret"); + var key = TryGetValue(jsonData, identifierKey ?? "apiKey"); + var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret"); - if (key == null || secret == null) - throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); + if (key == null || secret == null) + throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); - Key = CreateSecureString(key); - Secret = CreateSecureString(secret); - } + Key = key.ToSecureString(); + Secret = secret.ToSecureString(); inputStream.Seek(0, SeekOrigin.Begin); } @@ -94,26 +105,12 @@ namespace CryptoExchange.Net.Authentication /// /// /// - protected string TryGetValue(JToken data, string key) + protected string? TryGetValue(JToken data, string key) { if (data[key] == null) return null; return (string) data[key]; - } - - /// - /// Create a secure string from a string - /// - /// - /// - protected SecureString CreateSecureString(string source) - { - var secureString = new SecureString(); - foreach (var c in source) - secureString.AppendChar(c); - secureString.MakeReadOnly(); - return secureString; - } + } /// /// Dispose diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index d2a41a0..8739fed 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net.Http; namespace CryptoExchange.Net.Authentication { @@ -29,7 +30,7 @@ namespace CryptoExchange.Net.Authentication /// /// /// - public virtual Dictionary AddAuthenticationToParameters(string uri, string method, Dictionary parameters, bool signed) + public virtual Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed) { return parameters; } @@ -42,7 +43,7 @@ namespace CryptoExchange.Net.Authentication /// /// /// - public virtual Dictionary AddAuthenticationToHeaders(string uri, string method, Dictionary parameters, bool signed) + public virtual Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed) { return new Dictionary(); } diff --git a/CryptoExchange.Net/Authentication/PrivateKey.cs b/CryptoExchange.Net/Authentication/PrivateKey.cs index 47c4755..238e9b8 100644 --- a/CryptoExchange.Net/Authentication/PrivateKey.cs +++ b/CryptoExchange.Net/Authentication/PrivateKey.cs @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Authentication /// /// The private key's pass phrase /// - public SecureString Passphrase { get; } + public SecureString? Passphrase { get; } /// /// Indicates if the private key is encrypted or not @@ -81,15 +81,23 @@ namespace CryptoExchange.Net.Authentication if (string.IsNullOrEmpty(key)) throw new ArgumentException("Key can't be null/empty"); - var secureKey = new SecureString(); - foreach (var c in key) - secureKey.AppendChar(c); - secureKey.MakeReadOnly(); - Key = secureKey; + 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 /// diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index 6d48158..a4bbc98 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -7,20 +7,23 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; +using System.Text; +using System.Threading.Tasks; namespace CryptoExchange.Net { /// /// The base for all clients /// - public abstract class BaseClient: IDisposable + public abstract class BaseClient : IDisposable { /// /// The address of the client /// - public string BaseAddress { get; private set; } + public string BaseAddress { get; } /// /// The log object /// @@ -28,11 +31,11 @@ namespace CryptoExchange.Net /// /// The api proxy /// - protected ApiProxy apiProxy; + protected ApiProxy? apiProxy; /// /// The auth provider /// - protected internal AuthenticationProvider authProvider; + protected internal AuthenticationProvider? authProvider; /// /// The last used id @@ -59,26 +62,17 @@ namespace CryptoExchange.Net /// /// /// - protected BaseClient(ClientOptions options, AuthenticationProvider authenticationProvider) + protected BaseClient(ClientOptions options, AuthenticationProvider? authenticationProvider) { log = new Log(); authProvider = authenticationProvider; - Configure(options); - } + log.UpdateWriters(options.LogWriters); + log.Level = options.LogVerbosity; - /// - /// Configure the client using the provided options - /// - /// Options - protected void Configure(ClientOptions clientOptions) - { - log.UpdateWriters(clientOptions.LogWriters); - log.Level = clientOptions.LogVerbosity; + BaseAddress = options.BaseAddress; + apiProxy = options.Proxy; - BaseAddress = clientOptions.BaseAddress; - apiProxy = clientOptions.Proxy; - if (apiProxy != null) - log.Write(LogVerbosity.Info, $"Setting api proxy to {clientOptions.Proxy.Host}:{clientOptions.Proxy.Port}"); + log.Write(LogVerbosity.Debug, $"Client configuration: {options}"); } /// @@ -112,19 +106,16 @@ namespace CryptoExchange.Net catch (JsonReaderException jre) { var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {data}"; - log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } catch (JsonSerializationException jse) { var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {data}"; - log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } catch (Exception ex) { var info = $"Deserialize Unknown Exception: {ex.Message}. Data: {data}"; - log.Write(LogVerbosity.Error, info); return new CallResult(null, new DeserializeError(info)); } } @@ -137,10 +128,16 @@ namespace CryptoExchange.Net /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use /// - protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer serializer = null) + protected CallResult Deserialize(string data, bool checkObject = true, JsonSerializer? serializer = null) { var tokenResult = ValidateJson(data); - return !tokenResult.Success ? new CallResult(default(T), tokenResult.Error) : Deserialize(tokenResult.Data, checkObject, serializer); + if (!tokenResult) + { + log.Write(LogVerbosity.Error, tokenResult.Error!.Message); + return new CallResult(default, tokenResult.Error); + } + + return Deserialize(tokenResult.Data, checkObject, serializer); } /// @@ -151,7 +148,7 @@ namespace CryptoExchange.Net /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use /// - protected CallResult Deserialize(JToken obj, bool checkObject = true, JsonSerializer serializer = null) + protected CallResult Deserialize(JToken obj, bool checkObject = true, JsonSerializer? serializer = null) { if (serializer == null) serializer = defaultSerializer; @@ -184,22 +181,79 @@ namespace CryptoExchange.Net { var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {obj}"; log.Write(LogVerbosity.Error, info); - return new CallResult(default(T), new DeserializeError(info)); + return new CallResult(default, new DeserializeError(info)); } catch (JsonSerializationException jse) { var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {obj}"; log.Write(LogVerbosity.Error, info); - return new CallResult(default(T), new DeserializeError(info)); + return new CallResult(default, new DeserializeError(info)); } catch (Exception ex) { var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {obj}"; log.Write(LogVerbosity.Error, info); - return new CallResult(default(T), new DeserializeError(info)); + return new CallResult(default, new DeserializeError(info)); } } + /// + /// Deserialize a stream into an object + /// + /// The type to deserialize into + /// The stream to deserialize + /// A specific serializer to use + /// + protected async Task> Deserialize(Stream stream, JsonSerializer? serializer = null) + { + if (serializer == null) + serializer = defaultSerializer; + + try + { + using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); + if (log.Level == LogVerbosity.Debug) + { + var data = await reader.ReadToEndAsync().ConfigureAwait(false); + log.Write(LogVerbosity.Debug, $"Data received: {data}"); + return new CallResult(JsonConvert.DeserializeObject(data), null); + } + + using var jsonReader = new JsonTextReader(reader); + return new CallResult(serializer.Deserialize(jsonReader), null); + } + catch (JsonReaderException jre) + { + if(stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); + var data = await ReadStream(stream).ConfigureAwait(false); + log.Write(LogVerbosity.Error, $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); + } + catch (JsonSerializationException jse) + { + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); + var data = await ReadStream(stream).ConfigureAwait(false); + log.Write(LogVerbosity.Error, $"Deserialize JsonSerializationException: {jse.Message}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); + } + catch (Exception ex) + { + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); + var data = await ReadStream(stream).ConfigureAwait(false); + log.Write(LogVerbosity.Error, $"Deserialize Unknown Exception: {ex.Message}, data: {data}"); + return new CallResult(default, new DeserializeError(data)); + } + } + + private async Task ReadStream(Stream stream) + { + using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); + return await reader.ReadToEndAsync().ConfigureAwait(false); + } + private void CheckObject(Type type, JObject obj) { if (type.GetCustomAttribute(true) != null) @@ -233,13 +287,17 @@ namespace CryptoExchange.Net if (d == null) { d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase)); - if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) + if (d == null) { - log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); - isDif = true; + if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) + { + log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); + isDif = true; + } continue; } } + properties.Remove(d); var propType = GetProperty(d, props)?.PropertyType; @@ -270,14 +328,14 @@ namespace CryptoExchange.Net log.Write(LogVerbosity.Debug, "Returned data: " + obj); } - private static PropertyInfo GetProperty(string name, IEnumerable props) + private static PropertyInfo? GetProperty(string name, IEnumerable props) { foreach (var prop in props) { var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault(); if (attr == null) { - if (String.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase)) + if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase)) return prop; } else diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs index 62cebc6..4d7b8bc 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/ArrayConverter.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Globalization; using System.Linq; using System.Reflection; +using CryptoExchange.Net.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -20,7 +21,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (objectType == typeof(JToken)) return JToken.Load(reader); @@ -30,7 +31,7 @@ namespace CryptoExchange.Net.Converters return ParseObject(arr, result, objectType); } - private static object ParseObject(JArray arr, object result, Type objectType) + private static object? ParseObject(JArray arr, object result, Type objectType) { foreach (var property in objectType.GetProperties()) { @@ -74,7 +75,20 @@ namespace CryptoExchange.Net.Converters } var converterAttribute = (JsonConverterAttribute)property.GetCustomAttribute(typeof(JsonConverterAttribute)) ?? (JsonConverterAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConverterAttribute)); - var value = converterAttribute != null ? arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer { Converters = { (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType) } }) : arr[attribute.Index]; + var conversionAttribute = (JsonConversionAttribute)property.GetCustomAttribute(typeof(JsonConversionAttribute)) ?? (JsonConversionAttribute)property.PropertyType.GetCustomAttribute(typeof(JsonConversionAttribute)); + object? value; + if (converterAttribute != null) + { + value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); + } + else if (conversionAttribute != null) + { + value = arr[attribute.Index].ToObject(property.PropertyType); + } + else + { + value = arr[attribute.Index]; + } if (value != null && property.PropertyType.IsInstanceOfType(value)) property.SetValue(result, value); @@ -119,7 +133,7 @@ namespace CryptoExchange.Net.Converters while (arrayProp.Index != last + 1) { - writer.WriteValue((string)null); + writer.WriteValue((string?)null); last += 1; } diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index 47ce489..b21a521 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Converters /// Base class for enum converters /// /// Type of enum to convert - public abstract class BaseConverter: JsonConverter + public abstract class BaseConverter: JsonConverter where T: struct { /// /// The enum->string mapping @@ -38,7 +38,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; @@ -78,7 +78,7 @@ namespace CryptoExchange.Net.Converters return true; } - result = default(T); + result = default; return false; } diff --git a/CryptoExchange.Net/Converters/TimestampConverter.cs b/CryptoExchange.Net/Converters/TimestampConverter.cs index 6d08a0d..f1b9103 100644 --- a/CryptoExchange.Net/Converters/TimestampConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampConverter.cs @@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; diff --git a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs index 17a6803..80727b6 100644 --- a/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampNanoSecondsConverter.cs @@ -8,6 +8,8 @@ namespace CryptoExchange.Net.Converters /// public class TimestampNanoSecondsConverter : JsonConverter { + private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; + /// public override bool CanConvert(Type objectType) { @@ -15,12 +17,11 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; - var ticksPerNanosecond = (TimeSpan.TicksPerMillisecond / 1000m / 1000); var nanoSeconds = long.Parse(reader.Value.ToString()); return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerNanosecond)); } @@ -28,7 +29,6 @@ namespace CryptoExchange.Net.Converters /// public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var ticksPerNanosecond = (TimeSpan.TicksPerMillisecond / 1000m / 1000); writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).Ticks / ticksPerNanosecond)); } } diff --git a/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs b/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs index c1dff7e..1b2264a 100644 --- a/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampSecondsConverter.cs @@ -16,8 +16,11 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { + if (reader.Value == null) + return null; + if (reader.Value is double d) return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d); diff --git a/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs b/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs index 5210ccf..9690f7f 100644 --- a/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/UTCDateTimeConverter.cs @@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) return null; diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 51025d2..6c4f1cb 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,19 +1,22 @@ - netstandard2.0 + netstandard2.0;netstandard2.1 CryptoExchange.Net JKorf - 2.1.7 + A base package for implementing cryptocurrency exchange API's + 3.0.5 false https://github.com/JKorf/CryptoExchange.Net - https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE en true - 2.1.7 - Fixed bug with socket connection not being disposed after lost connection, Resubscribing after reconnecting socket now in parallel + 3.0.5 - Added PausedActivity events on socket subscriptions + enable + 8.0 + MIT - + CryptoExchange.Net.xml diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index fd9367b..97639e7 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -4,6 +4,11 @@ CryptoExchange.Net + + + Used for conversion in ArrayConverter + + Marks property as optional @@ -49,6 +54,12 @@ The api key used for identification The api secret used for signing + + + Copy the credentials + + + Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret @@ -65,13 +76,6 @@ - - - Create a secure string from a string - - - - Dispose @@ -93,7 +97,7 @@ - + Add authentication to the parameter list @@ -103,7 +107,7 @@ - + Add authentication to the header dictionary @@ -180,6 +184,12 @@ The private key used for signing + + + Copy the private key + + + Dispose @@ -232,12 +242,6 @@ - - - Configure the client using the provided options - - Options - Set the authentication provider @@ -271,6 +275,15 @@ A specific serializer to use + + + Deserialize a stream into an object + + The type to deserialize into + The stream to deserialize + A specific serializer to use + + Generate a unique id @@ -446,12 +459,13 @@ - + Create a query string of the specified parameters The parameters to use Whether or not the values should be url encoded + How to serialize array parameters @@ -461,11 +475,11 @@ The source secure string - + - Header collection to inenumerable + Create a secure string from a string - + @@ -493,6 +507,44 @@ + + + Validates an int is one of the allowed values + + Value of the int + Name of the parameter + Allowed values + + + + Validates an int is between two values + + The value of the int + Name of the parameter + Min value + Max value + + + + Validates a string is not null or empty + + The value of the string + Name of the parameter + + + + Validates an object is not null + + The value of the object + Name of the parameter + + + + Validates a list is not null or empty + + The value of the object + Name of the parameter + Rate limiter interface @@ -512,65 +564,51 @@ Request interface - + - The uri of the request - - - - - The headers of the request - - - - - The method of the request - - - - - The timeout of the request - - - - - Set a proxy - - - - - - - - - Content type + Accept header - String content + Content - + - Accept + Method - + - Content length + Uri - + - Get the request stream + Set byte content - + - + - Get the response object + Set string content + + + + + + Add a header to the request + + + + + + + Get the response + + @@ -578,13 +616,21 @@ Request factory interface - + Create a request for an uri + + + + Configure the requests created by this factory + + Request timeout to use + Proxy settings to use + Response object interface @@ -595,18 +641,22 @@ The response status code + + + Whether the status code indicates a success status + + + + + The response headers + + Get the response stream - - - Get the response headers - - - Close the response @@ -653,13 +703,13 @@ Removes all rate limiters from this client - + Ping to see if the server is reachable The roundtrip time of the ping request - + Ping to see if the server is reachable @@ -690,6 +740,20 @@ The base address of the API + + + + + + + + + The max amount of concurrent socket connections + + + + + Unsubscribe from a stream @@ -703,6 +767,125 @@ + + + Interface for order book + + + + + The status of the order book. Order book is up to date when the status is `Synced` + + + + + Last update identifier + + + + + The symbol of the order book + + + + + Event when the state changes + + + + + Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + + + + + Event when the BestBid or BestAsk changes ie a Pricing Tick + + + + + Timestamp of the last update + + + + + The number of asks in the book + + + + + The number of bids in the book + + + + + The list of asks + + + + + The list of bids + + + + + The best bid currently in the order book + + + + + The best ask currently in the order book + + + + + Start connecting and synchronizing the order book + + + + + + Start connecting and synchronizing the order book + + + + + + Stop syncing the order book + + + + + + Stop syncing the order book + + + + + + Interface for order book entries + + + + + The quantity of the entry + + + + + The price of the entry + + + + + Interface for order book entries + + + + + Sequence of the update + + Interface for websocket interaction @@ -773,16 +956,6 @@ Is open - - - Should ping connecting - - - - - Interval of pinging - - Supported ssl protocols @@ -980,6 +1153,16 @@ The proxy login The proxy password + + + + Create new settings for a proxy + + The proxy hostname/ip + The proxy port + The proxy login + The proxy password + Comparer for byte order @@ -1021,6 +1204,12 @@ + + + Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) + + + The result of a request @@ -1037,7 +1226,7 @@ The response headers - + ctor @@ -1053,7 +1242,7 @@ - + Create an error result @@ -1067,26 +1256,6 @@ Constants - - - GET Http method - - - - - POST Http method - - - - - DELETE Http method - - - - - PUT Http method - - Json content type header @@ -1182,6 +1351,21 @@ Bid + + + Define how array parameters should be send + + + + + Send multiple key=value for each entry + + + + + Create an []=value array + + Base class for errors @@ -1197,12 +1381,18 @@ The message for the error that occured - + + + Optional data for the error + + + ctor + @@ -1235,51 +1425,53 @@ Error returned by the server - + ctor + - + ctor + Web error returned by the server - + ctor - + Error while deserializing data - + ctor - + Deserializing data Unknown error - + ctor - + Error data @@ -1303,6 +1495,16 @@ + + + Cancellation requested + + + + + ctor + + Base options @@ -1318,6 +1520,9 @@ The log writers + + + Base for order book options @@ -1339,26 +1544,38 @@ The name of the order book implementation Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. + + + Base client options - - - The api credentials - - The base address of the client + + + The api credentials + + Proxy to use + + + ctor + + + + + + Base for rest client options @@ -1379,6 +1596,12 @@ The time the server has to respond to a request before timing out + + + ctor + + + Create a copy of the options @@ -1386,6 +1609,9 @@ + + + Base for socket client options @@ -1417,6 +1643,12 @@ Setting this to a higher number increases subscription speed, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection. + + + ctor + + + Create a copy of the options @@ -1424,89 +1656,68 @@ - - - Interface for order book entries - - - - - The quantity of the entry - - - - - The price of the entry - - - - - Order book entry - - - - - Quantity of the entry - - - - - Price of the entry - - - - - ctor - - - + + Buffer entry for order book - + - The first sequence number of the entries + List of asks - + - The last sequence number of the entries + List of bids - + - List of entries + Buffer entry with a single update id per update - + - ctor + First update id - + - Process entry for order book + List of asks - + - The entry + List of bids - + - The type + Buffer entry with a first and last update id - + - ctor + First update id + + + + + Last update id + + + + + List of asks + + + + + List of bids - - @@ -1528,11 +1739,21 @@ The bid list + + + Order book implementation id + + The log + + + If order book is set + + The status of the order book. Order book is up to date when the status is `Synced` @@ -1553,6 +1774,21 @@ Event when the state changes + + + Event when the BestBid or BestAsk changes ie a Pricing Tick + + + + + Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets + + + + + Timestamp of the last update + + The number of asks in the book @@ -1631,7 +1867,7 @@ - + Set the initial data for the order book @@ -1639,26 +1875,50 @@ List of asks List of bids - + - Update the order book with entries + Update the order book using a single id for an update - First sequence number - Last sequence number - List of entries + + + + + + + Update the order book using a first/last update id + + + + + + + + + Update the order book using sequenced entries + + List of bids + List of asks Check and empty the process buffer; see what entries to update the book with - + Update order book with an entry + Sequence number of the update Type of entry The entry + + + Wait until the order book has been set + + Max wait time + + Dispose the order book @@ -1756,17 +2016,12 @@ Request object - + - Create request object for webrequest + Create request object for web request - - - - - - + @@ -1774,25 +2029,22 @@ - - - - - - - + - + - + + + + @@ -1800,7 +2052,10 @@ WebRequest factory - + + + + @@ -1811,16 +2066,19 @@ - - - Create response for http web response - - - - + - + + + + + + Create response for a http response message + + The actual response + + @@ -1846,6 +2104,11 @@ Request body content type + + + How to serialize array parameters + + Timeout for requests @@ -1858,7 +2121,7 @@ - List of ratelimitters + List of rate limiters @@ -1873,12 +2136,6 @@ - - - Configure the client using the provided options - - Options - Adds a rate limiter to the client. There are 2 choices, the and the . @@ -1890,38 +2147,40 @@ Removes all rate limiters from this client - + Ping to see if the server is reachable The roundtrip time of the ping request - + Ping to see if the server is reachable The roundtrip time of the ping request - + Execute a request The expected result type The uri to send the request to The method of the request + Cancellation token The parameters of the request Whether or not the request should be authenticated Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) - + - Can be overridden to indicate if a response is an error response + Executes the request and returns the string result - The received data - True if error response + The request object to execute + Cancellation token + - + Creates a request object @@ -1931,26 +2190,13 @@ Whether or not the request should be authenticated - - - Writes the string data of the parameters to the request body stream - - - - - + Writes the parameters of the request to the request object, either in the query string or the request body - - - - Executes the request and returns the string result - - The request object to execute - + @@ -2041,12 +2287,6 @@ Client options Authentication provider - - - Configure the client using the provided options - - Options - Set a function to interpret the data, used when the data is received as bytes instead of a string @@ -2090,7 +2330,7 @@ Query for data - Exepected result type + Expected result type The request to send Whether the socket should be authenticated @@ -2253,6 +2493,187 @@ Socket implementation + + + Socket + + + + + Log + + + + + Error handlers + + + + + Open handlers + + + + + Close handlers + + + + + Message handlers + + + + + Id + + + + + If is reconnecting + + + + + Origin + + + + + Url + + + + + Is closed + + + + + Is open + + + + + Protocols + + + + + Interpreter for bytes + + + + + Interpreter for strings + + + + + Last action time + + + + + Timeout + + + + + Socket state + + + + + ctor + + + + + + + ctor + + + + + + + + + On close + + + + + On message + + + + + On error + + + + + On open + + + + + Handle + + + + + + Handle + + + + + + + + Checks if timed out + + + + + + Close socket + + + + + + Reset socket + + + + + Send data + + + + + + Connect socket + + + + + + Set a proxy + + + + + + + Dispose + + Socket connecting @@ -2268,6 +2689,16 @@ Connecting restored event + + + The connection is paused event + + + + + The connection is unpaused event + + Connecting closed event @@ -2315,24 +2746,11 @@ The socket client The socket - + - Add a handler + Add handler - The request object - If it is a user subscription or a generic handler - The data handler - - - - - Add a handler - - The identifier of the handler - If it is a user subscription or a generic handler - The data handler - - + @@ -2411,14 +2829,23 @@ If the subscription has been confirmed - + - ctor + Create SocketSubscription for a request - + + + + + Create SocketSubscription for an identifier + + + + + @@ -2441,6 +2868,16 @@ Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting + + + Event when the connection to the server is paused. No operations can be performed while paused + + + + + Event when the connection to the server is unpaused + + Event when an exception happened diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index f2485a8..1090cf9 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -8,6 +8,7 @@ using System.Security; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Logging; +using CryptoExchange.Net.Objects; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -46,7 +47,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddOptionalParameter(this Dictionary parameters, string key, object value) + public static void AddOptionalParameter(this Dictionary parameters, string key, object? value) { if(value != null) parameters.Add(key, value); @@ -58,7 +59,7 @@ namespace CryptoExchange.Net /// /// /// - public static void AddOptionalParameter(this Dictionary parameters, string key, string value) + public static void AddOptionalParameter(this Dictionary parameters, string key, string? value) { if (value != null) parameters.Add(key, value); @@ -69,14 +70,22 @@ namespace CryptoExchange.Net /// /// The parameters to use /// Whether or not the values should be url encoded + /// How to serialize array parameters /// - public static string CreateParamString(this Dictionary parameters, bool urlEncodeValues) + public static string CreateParamString(this Dictionary parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) { var uriString = ""; var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); foreach (var arrayEntry in arraysParameters) { - uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; + if(serializationType == ArrayParametersSerialization.Array) + uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; + else + { + var array = (Array)arrayEntry.Value; + uriString += string.Join("&", array.OfType().Select(a => $"{arrayEntry.Key}={WebUtility.UrlEncode(a.ToString())}")); + uriString += "&"; + } } uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? WebUtility.UrlEncode(s.Value.ToString()) : s.Value)}"))}"; @@ -118,20 +127,17 @@ namespace CryptoExchange.Net } /// - /// Header collection to inenumerable + /// Create a secure string from a string /// - /// + /// /// - public static IEnumerable> ToIEnumerable(this WebHeaderCollection headers) + public static SecureString ToSecureString(this string source) { - if (headers == null) - return null; - - return Enumerable - .Range(0, headers.Count) - .SelectMany(i => headers.GetValues(i) - .Select(v => Tuple.Create(headers.GetKey(i), v)) - ); + var secureString = new SecureString(); + foreach (var c in source) + secureString.AppendChar(c); + secureString.MakeReadOnly(); + return secureString; } /// @@ -143,8 +149,8 @@ namespace CryptoExchange.Net /// public static async Task WaitOneAsync(this WaitHandle handle, int millisecondsTimeout, CancellationToken cancellationToken) { - RegisteredWaitHandle registeredHandle = null; - CancellationTokenRegistration tokenRegistration = default(CancellationTokenRegistration); + RegisteredWaitHandle? registeredHandle = null; + CancellationTokenRegistration tokenRegistration = default; try { var tcs = new TaskCompletionSource(); @@ -183,7 +189,7 @@ namespace CryptoExchange.Net /// /// /// - public static JToken ToJToken(this string stringData, Log log = null) + public static JToken? ToJToken(this string stringData, Log? log = null) { if (string.IsNullOrEmpty(stringData)) return null; @@ -207,5 +213,66 @@ namespace CryptoExchange.Net return null; } } + + /// + /// Validates an int is one of the allowed values + /// + /// Value of the int + /// Name of the parameter + /// Allowed values + public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues) + { + if (!allowedValues.Contains(value)) + throw new ArgumentException( + $"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}"); + } + + /// + /// Validates an int is between two values + /// + /// The value of the int + /// Name of the parameter + /// Min value + /// Max value + public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue) + { + if (value < minValue || value > maxValue) + throw new ArgumentException( + $"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}"); + } + + /// + /// Validates a string is not null or empty + /// + /// The value of the string + /// Name of the parameter + public static void ValidateNotNull(this string value, string argumentName) + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentException($"No value provided for parameter {argumentName}"); + } + + /// + /// Validates an object is not null + /// + /// The value of the object + /// Name of the parameter + public static void ValidateNotNull(this object value, string argumentName) + { + if (value == null) + throw new ArgumentException($"No value provided for parameter {argumentName}"); + } + + /// + /// Validates a list is not null or empty + /// + /// The value of the object + /// Name of the parameter + public static void ValidateNotNull(this IEnumerable value, string argumentName) + { + if (value == null || !value.Any()) + throw new ArgumentException($"No values provided for parameter {argumentName}"); + } } } + diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 6c42ca5..9e819cb 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -1,6 +1,6 @@ using System; -using System.IO; -using System.Net; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces @@ -11,56 +11,44 @@ namespace CryptoExchange.Net.Interfaces public interface IRequest { /// - /// The uri of the request + /// Accept header + /// + string Accept { set; } + /// + /// Content + /// + string? Content { get; } + /// + /// Method + /// + HttpMethod Method { get; set; } + /// + /// Uri /// Uri Uri { get; } /// - /// The headers of the request + /// Set byte content /// - WebHeaderCollection Headers { get; set; } + /// + void SetContent(byte[] data); /// - /// The method of the request + /// Set string content /// - string Method { get; set; } - /// - /// The timeout of the request - /// - TimeSpan Timeout { get; set; } - /// - /// Set a proxy - /// - /// - /// - /// - /// - void SetProxy(string host, int port, string login, string password); + /// + /// + void SetContent(string data, string contentType); /// - /// Content type + /// Add a header to the request /// - string ContentType { get; set; } + /// + /// + void AddHeader(string key, string value); /// - /// String content - /// - string Content { get; set; } - /// - /// Accept - /// - string Accept { get; set; } - /// - /// Content length - /// - long ContentLength { get; set; } - - /// - /// Get the request stream + /// Get the response /// + /// /// - Task GetRequestStream(); - /// - /// Get the response object - /// - /// - Task GetResponse(); + Task GetResponse(CancellationToken cancellationToken); } } diff --git a/CryptoExchange.Net/Interfaces/IRequestFactory.cs b/CryptoExchange.Net/Interfaces/IRequestFactory.cs index 193db1d..ceb7f8c 100644 --- a/CryptoExchange.Net/Interfaces/IRequestFactory.cs +++ b/CryptoExchange.Net/Interfaces/IRequestFactory.cs @@ -1,4 +1,8 @@ -namespace CryptoExchange.Net.Interfaces +using CryptoExchange.Net.Objects; +using System; +using System.Net.Http; + +namespace CryptoExchange.Net.Interfaces { /// /// Request factory interface @@ -8,8 +12,16 @@ /// /// Create a request for an uri /// + /// /// /// - IRequest Create(string uri); + IRequest Create(HttpMethod method, string uri); + + /// + /// Configure the requests created by this factory + /// + /// Request timeout to use + /// Proxy settings to use + void Configure(TimeSpan requestTimeout, ApiProxy? proxy); } } diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index cc32604..9ebb888 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces { @@ -14,16 +14,23 @@ namespace CryptoExchange.Net.Interfaces /// The response status code /// HttpStatusCode StatusCode { get; } + + /// + /// Whether the status code indicates a success status + /// + bool IsSuccessStatusCode { get; } + + /// + /// The response headers + /// + IEnumerable>> ResponseHeaders { get; } + /// /// Get the response stream /// /// - Stream GetResponseStream(); - /// - /// Get the response headers - /// - /// - IEnumerable> GetResponseHeaders(); + Task GetResponseStream(); + /// /// Close the response /// diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index b3a8c4b..60d9e8b 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; @@ -52,12 +52,12 @@ namespace CryptoExchange.Net.Interfaces /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - CallResult Ping(); + CallResult Ping(CancellationToken ct = default); /// /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - Task> PingAsync(); + Task> PingAsync(CancellationToken ct = default); } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 195b1ce..7cbacc9 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; namespace CryptoExchange.Net.Interfaces @@ -29,6 +30,20 @@ namespace CryptoExchange.Net.Interfaces /// string BaseAddress { get; } + /// + TimeSpan ResponseTimeout { get; } + + /// + TimeSpan SocketNoDataTimeout { get; } + + /// + /// The max amount of concurrent socket connections + /// + int MaxSocketConnections { get; } + + /// + int SocketCombineTarget { get; } + /// /// Unsubscribe from a stream /// diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs new file mode 100644 index 0000000..9d0b502 --- /dev/null +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CryptoExchange.Net.Objects; + +namespace CryptoExchange.Net.Interfaces +{ + /// + /// Interface for order book + /// + public interface ISymbolOrderBook + { + /// + /// The status of the order book. Order book is up to date when the status is `Synced` + /// + OrderBookStatus Status { get; set; } + + /// + /// Last update identifier + /// + long LastSequenceNumber { get; } + /// + /// The symbol of the order book + /// + string Symbol { get; } + + /// + /// Event when the state changes + /// + event Action OnStatusChange; + /// + /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets + /// + event Action, IEnumerable> OnOrderBookUpdate; + /// + /// Event when the BestBid or BestAsk changes ie a Pricing Tick + /// + event Action OnBestOffersChanged; + /// + /// Timestamp of the last update + /// + DateTime LastOrderBookUpdate { get; } + + /// + /// The number of asks in the book + /// + int AskCount { get; } + /// + /// The number of bids in the book + /// + int BidCount { get; } + + /// + /// The list of asks + /// + IEnumerable Asks { get; } + + /// + /// The list of bids + /// + IEnumerable Bids { get; } + + /// + /// The best bid currently in the order book + /// + ISymbolOrderBookEntry BestBid { get; } + + /// + /// The best ask currently in the order book + /// + ISymbolOrderBookEntry BestAsk { get; } + + /// + /// Start connecting and synchronizing the order book + /// + /// + CallResult Start(); + + /// + /// Start connecting and synchronizing the order book + /// + /// + Task> StartAsync(); + + /// + /// Stop syncing the order book + /// + /// + void Stop(); + + /// + /// Stop syncing the order book + /// + /// + Task StopAsync(); + } +} diff --git a/CryptoExchange.Net/OrderBook/ISymbolOrderBookEntry.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs similarity index 53% rename from CryptoExchange.Net/OrderBook/ISymbolOrderBookEntry.cs rename to CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs index b5cb8ce..3aa9f97 100644 --- a/CryptoExchange.Net/OrderBook/ISymbolOrderBookEntry.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBookEntry.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.OrderBook +namespace CryptoExchange.Net.Interfaces { /// /// Interface for order book entries @@ -14,4 +14,15 @@ /// decimal Price { get; set; } } + + /// + /// Interface for order book entries + /// + public interface ISymbolOrderSequencedBookEntry: ISymbolOrderBookEntry + { + /// + /// Sequence of the update + /// + long Sequence { get; set; } + } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index 6a97da1..ede336e 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Origin /// - string Origin { get; set; } + string? Origin { get; set; } /// /// Reconnecting /// @@ -42,11 +42,11 @@ namespace CryptoExchange.Net.Interfaces /// /// Handler for byte data /// - Func DataInterpreterBytes { get; set; } + Func? DataInterpreterBytes { get; set; } /// /// Handler for string data /// - Func DataInterpreterString { get; set; } + Func? DataInterpreterString { get; set; } /// /// Socket url /// @@ -64,14 +64,6 @@ namespace CryptoExchange.Net.Interfaces /// bool IsOpen { get; } /// - /// Should ping connecting - /// - bool PingConnection { get; set; } - /// - /// Interval of pinging - /// - TimeSpan PingInterval { get; set; } - /// /// Supported ssl protocols /// SslProtocols SSLProtocols { get; set; } diff --git a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs index 5caac53..a9556dc 100644 --- a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs +++ b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs @@ -12,7 +12,7 @@ namespace CryptoExchange.Net.Logging private static readonly object openedFilesLock = new object(); private static readonly List openedFiles = new List(); - private StreamWriter logWriter; + private readonly StreamWriter logWriter; private readonly object writeLock; /// @@ -49,11 +49,8 @@ namespace CryptoExchange.Net.Logging /// protected override void Dispose(bool disposing) { - lock (writeLock) - { - logWriter.Close(); - logWriter = null; - } + lock (writeLock) + logWriter.Close(); } } } diff --git a/CryptoExchange.Net/Objects/ApiProxy.cs b/CryptoExchange.Net/Objects/ApiProxy.cs index b4db095..bbbd65e 100644 --- a/CryptoExchange.Net/Objects/ApiProxy.cs +++ b/CryptoExchange.Net/Objects/ApiProxy.cs @@ -1,4 +1,5 @@ using System; +using System.Security; namespace CryptoExchange.Net.Objects { @@ -19,25 +20,20 @@ namespace CryptoExchange.Net.Objects /// /// The login of the proxy /// - public string Login { get; } + public string? Login { get; } /// /// The password of the proxy /// - public string Password { get; } + public SecureString? Password { get; } /// /// Create new settings for a proxy /// /// The proxy hostname/ip /// The proxy port - public ApiProxy(string host, int port) + public ApiProxy(string host, int port): this(host, port, null, (SecureString?)null) { - if(string.IsNullOrEmpty(host) || port <= 0) - throw new ArgumentException("Proxy host or port not filled"); - - Host = host; - Port = port; } /// @@ -48,11 +44,25 @@ namespace CryptoExchange.Net.Objects /// The proxy port /// The proxy login /// The proxy password - public ApiProxy(string host, int port, string login, string password) : this(host, port) + public ApiProxy(string host, int port, string? login, string? password) : this(host, port, login, password?.ToSecureString()) { - if (string.IsNullOrEmpty(login) || string.IsNullOrEmpty(password)) - throw new ArgumentException("Proxy login or password not filled"); + } + /// + /// + /// Create new settings for a proxy + /// + /// The proxy hostname/ip + /// The proxy port + /// The proxy login + /// The proxy password + public ApiProxy(string host, int port, string? login, SecureString? password) + { + if (!host.StartsWith("http")) + throw new ArgumentException("Proxy host should start with either http:// or https://"); + + Host = host; + Port = port; Login = login; Password = password; } diff --git a/CryptoExchange.Net/Objects/ByteOrderComparer.cs b/CryptoExchange.Net/Objects/ByteOrderComparer.cs index 29be971..7ad3cbd 100644 --- a/CryptoExchange.Net/Objects/ByteOrderComparer.cs +++ b/CryptoExchange.Net/Objects/ByteOrderComparer.cs @@ -21,8 +21,8 @@ namespace CryptoExchange.Net.Objects // If one is null and the other isn't, then the // one that is null is "lesser". - if (x == null && y != null) return -1; - if (x != null && y == null) return 1; + if (x == null) return -1; + if (y == null) return 1; // Both arrays are non-null. Find the shorter // of the two lengths. diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index d8c2f59..9253ead 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -1,5 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; namespace CryptoExchange.Net.Objects @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects /// /// An error if the call didn't succeed /// - public Error Error { get; internal set; } + public Error? Error { get; internal set; } /// /// Whether the call was successful /// @@ -28,11 +28,20 @@ namespace CryptoExchange.Net.Objects /// /// /// - public CallResult(T data, Error error) + public CallResult([AllowNull]T data, Error? error) { Data = data; Error = error; } + + /// + /// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success) + /// + /// + public static implicit operator bool(CallResult obj) + { + return obj?.Success == true; + } } /// @@ -49,7 +58,7 @@ namespace CryptoExchange.Net.Objects /// /// The response headers /// - public IEnumerable> ResponseHeaders { get; set; } + public IEnumerable>>? ResponseHeaders { get; set; } /// /// ctor @@ -58,7 +67,9 @@ namespace CryptoExchange.Net.Objects /// /// /// - public WebCallResult(HttpStatusCode? code, IEnumerable> responseHeaders, T data, Error error): base(data, error) + public WebCallResult( + HttpStatusCode? code, + IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) { ResponseHeaders = responseHeaders; ResponseStatusCode = code; @@ -71,7 +82,7 @@ namespace CryptoExchange.Net.Objects /// public static WebCallResult CreateErrorResult(Error error) { - return new WebCallResult(null, null, default(T), error); + return new WebCallResult(null, null, default!, error); } /// @@ -81,9 +92,9 @@ namespace CryptoExchange.Net.Objects /// /// /// - public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable> responseHeaders, Error error) + public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, Error error) { - return new WebCallResult(code, responseHeaders, default(T), error); + return new WebCallResult(code, responseHeaders, default!, error); } } } diff --git a/CryptoExchange.Net/Objects/Constants.cs b/CryptoExchange.Net/Objects/Constants.cs index 55112fa..82e5af5 100644 --- a/CryptoExchange.Net/Objects/Constants.cs +++ b/CryptoExchange.Net/Objects/Constants.cs @@ -5,23 +5,6 @@ /// public class Constants { - /// - /// GET Http method - /// - public const string GetMethod = "GET"; - /// - /// POST Http method - /// - public const string PostMethod = "POST"; - /// - /// DELETE Http method - /// - public const string DeleteMethod = "DELETE"; - /// - /// PUT Http method - /// - public const string PutMethod = "PUT"; - /// /// Json content type header /// diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs index fc6b630..3af3694 100644 --- a/CryptoExchange.Net/Objects/Enums.cs +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -65,7 +65,7 @@ /// /// Data synced, order book is up to date /// - Synced, + Synced } /// @@ -82,4 +82,19 @@ /// Bid } + + /// + /// Define how array parameters should be send + /// + public enum ArrayParametersSerialization + { + /// + /// Send multiple key=value for each entry + /// + MultipleValues, + /// + /// Create an []=value array + /// + Array + } } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 29240b1..0a4b838 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -14,15 +14,22 @@ /// public string Message { get; set; } + /// + /// Optional data for the error + /// + public object? Data { get; set; } + /// /// ctor /// /// /// - protected Error(int code, string message) + /// + protected Error(int code, string message, object? data) { Code = code; Message = message; + Data = data; } /// @@ -31,7 +38,7 @@ /// public override string ToString() { - return $"{Code}: {Message}"; + return $"{Code}: {Message} {Data}"; } } @@ -43,7 +50,7 @@ /// /// ctor /// - public CantConnectError() : base(1, "Can't connect to the server") { } + public CantConnectError() : base(1, "Can't connect to the server", null) { } } /// @@ -54,7 +61,7 @@ /// /// ctor /// - public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint") { } + public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint", null) { } } /// @@ -66,14 +73,16 @@ /// ctor /// /// - public ServerError(string message) : base(3, "Server error: " + message) { } + /// + public ServerError(string message, object? data = null) : base(3, "Server error: " + message, data) { } /// /// ctor /// /// /// - public ServerError(int code, string message) : base(code, message) + /// + public ServerError(int code, string message, object? data = null) : base(code, message, data) { } } @@ -86,8 +95,8 @@ /// /// ctor /// - /// - public WebError(string message) : base(4, "Web error: " + message) { } + /// + public WebError(object? data) : base(4, "Web error", data) { } } /// @@ -98,8 +107,8 @@ /// /// ctor /// - /// - public DeserializeError(string message) : base(5, "Error deserializing data: " + message) { } + /// Deserializing data + public DeserializeError(object? data) : base(5, "Error deserializing data", data) { } } /// @@ -110,8 +119,8 @@ /// /// ctor /// - /// - public UnknownError(string message) : base(6, "Unknown error occured " + message) { } + /// Error data + public UnknownError(object? data = null) : base(6, "Unknown error occured", data) { } } /// @@ -123,7 +132,7 @@ /// ctor /// /// - public ArgumentError(string message) : base(7, "Invalid parameter: " + message) { } + public ArgumentError(string message) : base(7, "Invalid parameter: " + message, null) { } } /// @@ -135,6 +144,17 @@ /// ctor /// /// - public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { } + public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message, null) { } + } + + /// + /// Cancellation requested + /// + public class CancellationRequestedError : Error + { + /// + /// ctor + /// + public CancellationRequestedError() : base(9, "Cancellation requested", null) { } } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 525f1c8..18744ab 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -21,13 +21,19 @@ namespace CryptoExchange.Net.Objects /// The log writers /// public List LogWriters { get; set; } = new List { new DebugTextWriter() }; + + /// + public override string ToString() + { + return $"LogVerbosity: {LogVerbosity}, Writers: {LogWriters.Count}"; + } } /// /// Base for order book options /// - public class OrderBookOptions: BaseOptions - { + public class OrderBookOptions : BaseOptions + { /// /// The name of the order book implementation /// @@ -43,38 +49,59 @@ namespace CryptoExchange.Net.Objects /// The name of the order book implementation /// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. public OrderBookOptions(string name, bool sequencesAreConsecutive) - { + { OrderBookName = name; SequenceNumbersAreConsecutive = sequencesAreConsecutive; } + + /// + public override string ToString() + { + return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}"; + } } /// /// Base client options /// - public class ClientOptions: BaseOptions + public class ClientOptions : BaseOptions { - - /// - /// The api credentials - /// - public ApiCredentials ApiCredentials { get; set; } - /// /// The base address of the client /// public string BaseAddress { get; set; } + /// + /// The api credentials + /// + public ApiCredentials? ApiCredentials { get; set; } + + /// /// Proxy to use /// - public ApiProxy Proxy { get; set; } + public ApiProxy? Proxy { get; set; } + + /// + /// ctor + /// + /// + public ClientOptions(string baseAddress) + { + BaseAddress = baseAddress; + } + + /// + public override string ToString() + { + return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-": "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null? "-": Proxy.Host)}"; + } } /// /// Base for rest client options /// - public class RestClientOptions: ClientOptions + public class RestClientOptions : ClientOptions { /// /// List of rate limiters to use @@ -91,12 +118,20 @@ namespace CryptoExchange.Net.Objects /// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// + /// ctor + /// + /// + public RestClientOptions(string baseAddress): base(baseAddress) + { + } + /// /// Create a copy of the options /// /// /// - public T Copy() where T:RestClientOptions, new() + public T Copy() where T : RestClientOptions, new() { var copy = new T { @@ -110,16 +145,22 @@ namespace CryptoExchange.Net.Objects }; if (ApiCredentials != null) - copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString()); + copy.ApiCredentials = ApiCredentials.Copy(); return copy; } + + /// + public override string ToString() + { + return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}"; + } } /// /// Base for socket client options /// - public class SocketClientOptions: ClientOptions + public class SocketClientOptions : ClientOptions { /// /// Whether or not the socket should automatically reconnect when losing connection @@ -146,6 +187,14 @@ namespace CryptoExchange.Net.Objects /// public int? SocketSubscriptionsCombineTarget { get; set; } + /// + /// ctor + /// + /// + public SocketClientOptions(string baseAddress) : base(baseAddress) + { + } + /// /// Create a copy of the options /// @@ -166,9 +215,15 @@ namespace CryptoExchange.Net.Objects }; if (ApiCredentials != null) - copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString()); + copy.ApiCredentials = ApiCredentials.Copy(); return copy; } + + /// + public override string ToString() + { + return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; + } } } diff --git a/CryptoExchange.Net/OrderBook/OrderBookEntry.cs b/CryptoExchange.Net/OrderBook/OrderBookEntry.cs deleted file mode 100644 index 3faccf2..0000000 --- a/CryptoExchange.Net/OrderBook/OrderBookEntry.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace CryptoExchange.Net.OrderBook -{ - /// - /// Order book entry - /// - public class OrderBookEntry : ISymbolOrderBookEntry - { - /// - /// Quantity of the entry - /// - public decimal Quantity { get; set; } - /// - /// Price of the entry - /// - public decimal Price { get; set; } - - /// - /// ctor - /// - /// - /// - public OrderBookEntry(decimal price, decimal quantity) - { - Quantity = quantity; - Price = price; - } - } -} diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 8a48794..3cf64bc 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using CryptoExchange.Net.Interfaces; +using System.Collections.Generic; namespace CryptoExchange.Net.OrderBook { @@ -8,24 +9,54 @@ namespace CryptoExchange.Net.OrderBook public class ProcessBufferEntry { /// - /// The first sequence number of the entries + /// List of asks /// - public long FirstSequence { get; set; } + public IEnumerable Asks { get; set; } = new List(); /// - /// The last sequence number of the entries + /// List of bids /// - public long LastSequence { get; set; } - /// - /// List of entries - /// - public List Entries { get; set; } + public IEnumerable Bids { get; set; } = new List(); + } + /// + /// Buffer entry with a single update id per update + /// + public class ProcessBufferSingleSequenceEntry + { /// - /// ctor + /// First update id /// - public ProcessBufferEntry() - { - Entries = new List(); - } + public long UpdateId { get; set; } + /// + /// List of asks + /// + public IEnumerable Asks { get; set; } = new List(); + /// + /// List of bids + /// + public IEnumerable Bids { get; set; } = new List(); + } + + /// + /// Buffer entry with a first and last update id + /// + public class ProcessBufferRangeSequenceEntry + { + /// + /// First update id + /// + public long FirstUpdateId { get; set; } + /// + /// Last update id + /// + public long LastUpdateId { get; set; } + /// + /// List of asks + /// + public IEnumerable Asks { get; set; } = new List(); + /// + /// List of bids + /// + public IEnumerable Bids { get; set; } = new List(); } } diff --git a/CryptoExchange.Net/OrderBook/ProcessEntry.cs b/CryptoExchange.Net/OrderBook/ProcessEntry.cs deleted file mode 100644 index a59bc5f..0000000 --- a/CryptoExchange.Net/OrderBook/ProcessEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.OrderBook -{ - /// - /// Process entry for order book - /// - public class ProcessEntry - { - /// - /// The entry - /// - public ISymbolOrderBookEntry Entry { get; set; } - /// - /// The type - /// - public OrderBookEntryType Type { get; set; } - - /// - /// ctor - /// - /// - /// - public ProcessEntry(OrderBookEntryType type, ISymbolOrderBookEntry entry) - { - Type = type; - Entry = entry; - } - } -} diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index e6c2e29..36e94b8 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; @@ -13,36 +14,44 @@ namespace CryptoExchange.Net.OrderBook /// /// Base for order book implementations /// - public abstract class SymbolOrderBook: IDisposable + public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable { /// /// The process buffer, used while syncing /// - protected readonly List processBuffer; + protected readonly List processBuffer; private readonly object bookLock = new object(); /// /// The ask list /// - protected SortedList asks; + protected SortedList asks; /// /// The bid list /// - protected SortedList bids; + + protected SortedList bids; private OrderBookStatus status; - private UpdateSubscription subscription; + private UpdateSubscription? subscription; private readonly bool sequencesAreConsecutive; - private readonly string id; + + /// + /// Order book implementation id + /// + public string Id { get; } /// /// The log /// protected Log log; - private bool bookSet; + /// + /// If order book is set + /// + protected bool bookSet; /// /// The status of the order book. Order book is up to date when the status is `Synced` /// - public OrderBookStatus Status + public OrderBookStatus Status { get => status; set @@ -52,7 +61,7 @@ namespace CryptoExchange.Net.OrderBook var old = status; status = value; - log.Write(LogVerbosity.Info, $"{id} order book {Symbol} status changed: {old} => {value}"); + log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} status changed: {old} => {value}"); OnStatusChange?.Invoke(old, status); } } @@ -69,7 +78,21 @@ namespace CryptoExchange.Net.OrderBook /// /// Event when the state changes /// - public event Action OnStatusChange; + public event Action? OnStatusChange; + + /// + /// Event when the BestBid or BestAsk changes ie a Pricing Tick + /// + public event Action? OnBestOffersChanged; + + /// + /// Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets + /// + public event Action, IEnumerable>? OnOrderBookUpdate; + /// + /// Timestamp of the last update + /// + public DateTime LastOrderBookUpdate { get; private set; } /// /// The number of asks in the book @@ -95,7 +118,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The list of bids /// - public IEnumerable Bids + public IEnumerable Bids { get { @@ -119,7 +142,7 @@ namespace CryptoExchange.Net.OrderBook /// /// The best ask currently in the order book /// - public ISymbolOrderBookEntry BestAsk + public ISymbolOrderBookEntry BestAsk { get { @@ -135,14 +158,20 @@ namespace CryptoExchange.Net.OrderBook /// protected SymbolOrderBook(string symbol, OrderBookOptions options) { - id = options.OrderBookName; - processBuffer = new List(); + if (symbol == null) + throw new ArgumentNullException(nameof(symbol)); + + if (options == null) + throw new ArgumentNullException(nameof(options)); + + Id = options.OrderBookName; + processBuffer = new List(); sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; Symbol = symbol; Status = OrderBookStatus.Disconnected; - asks = new SortedList(); - bids = new SortedList(new DescComparer()); + asks = new SortedList(); + bids = new SortedList(new DescComparer()); log = new Log { Level = options.LogVerbosity }; var writers = options.LogWriters ?? new List { new DebugTextWriter() }; @@ -163,19 +192,19 @@ namespace CryptoExchange.Net.OrderBook { Status = OrderBookStatus.Connecting; var startResult = await DoStart().ConfigureAwait(false); - if(!startResult.Success) + if (!startResult) return new CallResult(false, startResult.Error); subscription = startResult.Data; subscription.ConnectionLost += Reset; - subscription.ConnectionRestored += (time) => Resync(); + subscription.ConnectionRestored += time => Resync(); Status = OrderBookStatus.Synced; return new CallResult(true, null); } private void Reset() { - log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} connection lost"); + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost"); Status = OrderBookStatus.Connecting; processBuffer.Clear(); bookSet = false; @@ -185,17 +214,17 @@ namespace CryptoExchange.Net.OrderBook private void Resync() { Status = OrderBookStatus.Syncing; - bool success = false; + var success = false; while (!success) { if (Status != OrderBookStatus.Syncing) return; var resyncResult = DoResync().Result; - success = resyncResult.Success; + success = resyncResult; } - log.Write(LogVerbosity.Info, $"{id} order book {Symbol} successfully resynchronized"); + log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} successfully resynchronized"); Status = OrderBookStatus.Synced; } @@ -212,7 +241,8 @@ namespace CryptoExchange.Net.OrderBook public async Task StopAsync() { Status = OrderBookStatus.Disconnected; - await subscription.Close().ConfigureAwait(false); + if(subscription != null) + await subscription.Close().ConfigureAwait(false); } /// @@ -231,132 +261,295 @@ namespace CryptoExchange.Net.OrderBook /// /// protected abstract Task> DoResync(); - + /// /// Set the initial data for the order book /// /// The last update sequence number /// List of asks /// List of bids - protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable askList, IEnumerable bidList) + protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable bidList, IEnumerable askList) { lock (bookLock) { - if (Status == OrderBookStatus.Connecting) + if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) return; asks.Clear(); - foreach(var ask in askList) - asks.Add(ask.Price, new OrderBookEntry(ask.Price, ask.Quantity)); + foreach (var ask in askList) + asks.Add(ask.Price, ask); bids.Clear(); foreach (var bid in bidList) - bids.Add(bid.Price, new OrderBookEntry(bid.Price, bid.Quantity)); + bids.Add(bid.Price, bid); LastSequenceNumber = orderBookSequenceNumber; AskCount = asks.Count; BidCount = asks.Count; - CheckProcessBuffer(); bookSet = true; - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks"); + LastOrderBookUpdate = DateTime.UtcNow; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{orderBookSequenceNumber}"); + CheckProcessBuffer(); + OnOrderBookUpdate?.Invoke(bidList, askList); + OnBestOffersChanged?.Invoke(BestBid, BestAsk); } } + private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) + { + if (BestBid.Price != prevBestBid.Price || BestBid.Quantity != prevBestBid.Quantity || + BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity) + OnBestOffersChanged?.Invoke(BestBid, BestAsk); + } + /// - /// Update the order book with entries + /// Update the order book using a single id for an update /// - /// First sequence number - /// Last sequence number - /// List of entries - protected void UpdateOrderBook(long firstSequenceNumber, long lastSequenceNumber, List entries) + /// + /// + /// + protected void UpdateOrderBook(long rangeUpdateId, IEnumerable bids, IEnumerable asks) { lock (bookLock) { - if (lastSequenceNumber < LastSequenceNumber) + if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) return; if (!bookSet) { - var entry = new ProcessBufferEntry() + processBuffer.Add(new ProcessBufferSingleSequenceEntry() { - FirstSequence = firstSequenceNumber, - LastSequence = lastSequenceNumber, - Entries = entries - }; - processBuffer.Add(entry); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update before synced; buffering"); - } - else if (sequencesAreConsecutive && firstSequenceNumber > LastSequenceNumber + 1) - { - // Out of sync - log.Write(LogVerbosity.Warning, $"{id} order book {Symbol} out of sync, reconnecting"); - subscription.Reconnect().Wait(); + UpdateId = rangeUpdateId, + Asks = asks, + Bids = bids + }); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{rangeUpdateId}"); } else { - foreach(var entry in entries) - ProcessUpdate(entry.Type, entry.Entry); - LastSequenceNumber = lastSequenceNumber; CheckProcessBuffer(); - log.Write(LogVerbosity.Debug, $"{id} order book {Symbol} update: {entries.Count} entries processed"); + var prevBestBid = BestBid; + var prevBestAsk = BestAsk; + ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks); + OnOrderBookUpdate?.Invoke(bids, asks); + CheckBestOffersChanged(prevBestBid, prevBestAsk); } } } + /// + /// Update the order book using a first/last update id + /// + /// + /// + /// + /// + protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + { + lock (bookLock) + { + if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) + return; + + if (!bookSet) + { + processBuffer.Add(new ProcessBufferRangeSequenceEntry() + { + Asks = asks, + Bids = bids, + FirstUpdateId = firstUpdateId, + LastUpdateId = lastUpdateId + }); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{firstUpdateId}-{lastUpdateId}"); + } + else + { + CheckProcessBuffer(); + var prevBestBid = BestBid; + var prevBestAsk = BestAsk; + ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks); + OnOrderBookUpdate?.Invoke(bids, asks); + CheckBestOffersChanged(prevBestBid, prevBestAsk); + } + } + } + + /// + /// Update the order book using sequenced entries + /// + /// List of bids + /// List of asks + protected void UpdateOrderBook(IEnumerable bids, IEnumerable asks) + { + lock (bookLock) + { + if (!bookSet) + { + processBuffer.Add(new ProcessBufferEntry + { + Asks = asks, + Bids = bids + }); + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{Math.Min(bids.Min(b => b.Sequence), asks.Min(a => a.Sequence))}-{Math.Max(bids.Max(b => b.Sequence), asks.Max(a => a.Sequence))}"); + } + else + { + CheckProcessBuffer(); + var prevBestBid = BestBid; + var prevBestAsk = BestAsk; + ProcessUpdates(bids, asks); + OnOrderBookUpdate?.Invoke(bids, asks); + CheckBestOffersChanged(prevBestBid, prevBestAsk); + } + } + } + + private void ProcessUpdates(IEnumerable bids, IEnumerable asks) + { + var entries = new Dictionary(); + foreach (var entry in asks.OrderBy(a => a.Sequence)) + entries.Add(entry, OrderBookEntryType.Ask); + foreach (var entry in bids.OrderBy(a => a.Sequence)) + entries.Add(entry, OrderBookEntryType.Bid); + + foreach (var entry in entries.OrderBy(e => e.Key.Sequence)) + { + if(ProcessUpdate(entry.Key.Sequence, entry.Value, entry.Key)) + LastSequenceNumber = entry.Key.Sequence; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update #{LastSequenceNumber}"); + } + } + + private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + { + if (lastUpdateId < LastSequenceNumber) + { + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}"); + return; + } + + foreach (var entry in bids) + ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry); + + foreach (var entry in asks) + ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry); + + LastSequenceNumber = lastUpdateId; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}"); + } + + private void ProcessSingleSequenceUpdates(long updateId, IEnumerable bids, IEnumerable asks) + { + foreach (var entry in bids) + { + if (!ProcessUpdate(updateId, OrderBookEntryType.Bid, entry)) + return; + } + + foreach (var entry in asks) + { + if (!ProcessUpdate(updateId, OrderBookEntryType.Ask, entry)) + return; + } + + LastSequenceNumber = updateId; + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{LastSequenceNumber}"); + } + /// /// Check and empty the process buffer; see what entries to update the book with /// protected void CheckProcessBuffer() { - foreach (var bufferEntry in processBuffer.OrderBy(b => b.FirstSequence).ToList()) + var pbList = processBuffer.ToList(); + if(pbList.Count > 0) + log.Write(LogVerbosity.Debug, "Processing buffered updates"); + + foreach (var bufferEntry in pbList) { - if(bufferEntry.LastSequence < LastSequenceNumber) - { - processBuffer.Remove(bufferEntry); - continue; - } + if (bufferEntry is ProcessBufferEntry pbe) + ProcessUpdates(pbe.Bids, pbe.Asks); + else if(bufferEntry is ProcessBufferRangeSequenceEntry pbrse) + ProcessRangeUpdates(pbrse.FirstUpdateId, pbrse.LastUpdateId, pbrse.Bids, pbrse.Asks); + else if (bufferEntry is ProcessBufferSingleSequenceEntry pbsse) + ProcessSingleSequenceUpdates(pbsse.UpdateId, pbsse.Bids, pbsse.Asks); - if (bufferEntry.FirstSequence > LastSequenceNumber + 1) - break; - - foreach(var entry in bufferEntry.Entries) - ProcessUpdate(entry.Type, entry.Entry); processBuffer.Remove(bufferEntry); - LastSequenceNumber = bufferEntry.LastSequence; } } /// /// Update order book with an entry /// + /// Sequence number of the update /// Type of entry /// The entry - protected virtual void ProcessUpdate(OrderBookEntryType type, ISymbolOrderBookEntry entry) + protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry) { + if (Status != OrderBookStatus.Syncing && Status != OrderBookStatus.Synced) + return false; + + if (sequence <= LastSequenceNumber) + { + log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{sequence}"); + return false; + } + + if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1) + { + // Out of sync + log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); + Status = OrderBookStatus.Connecting; + subscription?.Reconnect(); + return false; + } + + LastOrderBookUpdate = DateTime.UtcNow; var listToChange = type == OrderBookEntryType.Ask ? asks : bids; if (entry.Quantity == 0) { - var bookEntry = listToChange.SingleOrDefault(i => i.Key == entry.Price); - if (!bookEntry.Equals(default(KeyValuePair))) - { - listToChange.Remove(entry.Price); - if (type == OrderBookEntryType.Ask) AskCount--; - else BidCount--; - } + if (!listToChange.ContainsKey(entry.Price)) + return true; + + listToChange.Remove(entry.Price); + if (type == OrderBookEntryType.Ask) AskCount--; + else BidCount--; } else { - var bookEntry = listToChange.SingleOrDefault(i => i.Key == entry.Price); - if (bookEntry.Equals(default(KeyValuePair))) + if (!listToChange.ContainsKey(entry.Price)) { - listToChange.Add(entry.Price, new OrderBookEntry(entry.Price, entry.Quantity)); + listToChange.Add(entry.Price, entry); if (type == OrderBookEntryType.Ask) AskCount++; else BidCount++; } else - bookEntry.Value.Quantity = entry.Quantity; + { + listToChange[entry.Price].Quantity = entry.Quantity; + } } + + return true; + } + + /// + /// Wait until the order book has been set + /// + /// Max wait time + /// + protected async Task> WaitForSetOrderBook(int timeout) + { + var startWait = DateTime.UtcNow; + while (!bookSet && Status == OrderBookStatus.Syncing) + { + if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout) + return new CallResult(false, new ServerError("Timeout while waiting for data")); + + await Task.Delay(10).ConfigureAwait(false); + } + + return new CallResult(true, null); } /// diff --git a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs index ce8d9e0..4bd9af3 100644 --- a/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs +++ b/CryptoExchange.Net/RateLimiter/RateLimiterAPIKey.cs @@ -32,10 +32,10 @@ namespace CryptoExchange.Net.RateLimiter /// public CallResult LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour) { - if(client.authProvider?.Credentials == null) + if(client.authProvider?.Credentials?.Key == null) return new CallResult(0, null); - string key = client.authProvider.Credentials.Key.GetString(); + var key = client.authProvider.Credentials.Key.GetString(); int waitTime; RateLimitObject rlo; diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 5566699..5c4530c 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,6 +1,8 @@ using System; -using System.IO; -using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; @@ -11,82 +13,62 @@ namespace CryptoExchange.Net.Requests /// public class Request : IRequest { - private readonly WebRequest request; + private readonly HttpRequestMessage request; + private readonly HttpClient httpClient; /// - /// Create request object for webrequest + /// Create request object for web request /// /// - public Request(WebRequest request) + /// + public Request(HttpRequestMessage request, HttpClient client) { + httpClient = client; this.request = request; } - + /// - public WebHeaderCollection Headers - { - get => request.Headers; - set => request.Headers = value; - } - - /// - public string ContentType - { - get => request.ContentType; - set => request.ContentType = value; - } - - /// - public string Content { get; set; } + public string? Content { get; private set; } /// public string Accept { - get => ((HttpWebRequest)request).Accept; - set => ((HttpWebRequest)request).Accept = value; + set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value)); } /// - public long ContentLength - { - get => ((HttpWebRequest)request).ContentLength; - set => ((HttpWebRequest)request).ContentLength = value; - } - - /// - public string Method + public HttpMethod Method { get => request.Method; set => request.Method = value; } - /// - public TimeSpan Timeout - { - get => TimeSpan.FromMilliseconds(request.Timeout); - set => request.Timeout = (int)Math.Round(value.TotalMilliseconds); - } - /// public Uri Uri => request.RequestUri; /// - public void SetProxy(string host, int port, string login, string password) + public void SetContent(string data, string contentType) { - request.Proxy = new WebProxy(host, port); - if(!string.IsNullOrEmpty(login) && !string.IsNullOrEmpty(password)) request.Proxy.Credentials = new NetworkCredential(login, password); + Content = data; + request.Content = new StringContent(data, Encoding.UTF8, contentType); } /// - public async Task GetRequestStream() + public void AddHeader(string key, string value) { - return await request.GetRequestStreamAsync().ConfigureAwait(false); + request.Headers.Add(key, value); } /// - public async Task GetResponse() + public void SetContent(byte[] data) { - return new Response((HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false)); + request.Content = new ByteArrayContent(data); + } + + /// + public async Task GetResponse(CancellationToken cancellationToken) + { + return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); } } } diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index add3f81..2434ca1 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -1,5 +1,8 @@ -using System.Net; +using System; +using System.Net; +using System.Net.Http; using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.Requests { @@ -8,10 +11,30 @@ namespace CryptoExchange.Net.Requests /// public class RequestFactory : IRequestFactory { + private HttpClient? httpClient; + /// - public IRequest Create(string uri) + public void Configure(TimeSpan requestTimeout, ApiProxy? proxy) { - return new Request(WebRequest.Create(uri)); + HttpMessageHandler handler = new HttpClientHandler() + { + Proxy = proxy == null ? null : new WebProxy + { + Address = new Uri($"{proxy.Host}:{proxy.Port}"), + Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) + } + }; + + httpClient = new HttpClient(handler) {Timeout = requestTimeout}; + } + + /// + public IRequest Create(HttpMethod method, string uri) + { + if (httpClient == null) + throw new InvalidOperationException("Cant create request before configuring http client"); + + return new Request(new HttpRequestMessage(method, uri), httpClient); } } } diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index a5d88a1..3df463d 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -1,7 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; namespace CryptoExchange.Net.Requests @@ -9,38 +10,38 @@ namespace CryptoExchange.Net.Requests /// /// HttpWebResponse response object /// - public class Response : IResponse + internal class Response : IResponse { - private readonly HttpWebResponse response; + private readonly HttpResponseMessage response; /// public HttpStatusCode StatusCode => response.StatusCode; + /// + public bool IsSuccessStatusCode => response.IsSuccessStatusCode; + + /// + public IEnumerable>> ResponseHeaders => response.Headers; + /// - /// Create response for http web response + /// Create response for a http response message /// - /// - public Response(HttpWebResponse response) + /// The actual response + public Response(HttpResponseMessage response) { this.response = response; } /// - public Stream GetResponseStream() + public async Task GetResponseStream() { - return response.GetResponseStream(); - } - - /// - public IEnumerable> GetResponseHeaders() - { - return response.Headers.ToIEnumerable(); + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } /// public void Close() { - response.Close(); + response.Dispose(); } } } diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 844607f..048cf6c 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net; +using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Web; using CryptoExchange.Net.Authentication; @@ -29,7 +30,6 @@ namespace CryptoExchange.Net /// public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); - /// /// Where to place post parameters /// @@ -39,16 +39,21 @@ namespace CryptoExchange.Net /// protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json; + /// + /// How to serialize array parameters + /// + protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array; + /// /// Timeout for requests /// - protected TimeSpan RequestTimeout { get; private set; } + public TimeSpan RequestTimeout { get; } /// /// Rate limiting behaviour /// - public RateLimitingBehaviour RateLimitBehaviour { get; private set; } + public RateLimitingBehaviour RateLimitBehaviour { get; } /// - /// List of ratelimitters + /// List of rate limiters /// public IEnumerable RateLimiters { get; private set; } /// @@ -61,18 +66,13 @@ namespace CryptoExchange.Net /// /// /// - protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider) + protected RestClient(RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { - Configure(exchangeOptions); - } + if (exchangeOptions == null) + throw new ArgumentNullException(nameof(exchangeOptions)); - /// - /// Configure the client using the provided options - /// - /// Options - protected void Configure(RestClientOptions exchangeOptions) - { RequestTimeout = exchangeOptions.RequestTimeout; + RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy); RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour; var rateLimiters = new List(); foreach (var rateLimiter in exchangeOptions.RateLimiters) @@ -86,6 +86,9 @@ namespace CryptoExchange.Net /// The limiter to add public void AddRateLimiter(IRateLimiter limiter) { + if (limiter == null) + throw new ArgumentNullException(nameof(limiter)); + var rateLimiters = RateLimiters.ToList(); rateLimiters.Add(limiter); RateLimiters = rateLimiters; @@ -103,17 +106,19 @@ namespace CryptoExchange.Net /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - public virtual CallResult Ping() => PingAsync().Result; + public virtual CallResult Ping(CancellationToken ct = default) => PingAsync(ct).Result; /// /// Ping to see if the server is reachable /// /// The roundtrip time of the ping request - public virtual async Task> PingAsync() + public virtual async Task> PingAsync(CancellationToken ct = default) { var ping = new Ping(); var uri = new Uri(BaseAddress); PingReply reply; + + var ctRegistration = ct.Register(() => ping.SendAsyncCancel()); try { reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false); @@ -121,14 +126,22 @@ namespace CryptoExchange.Net catch(PingException e) { if (e.InnerException == null) - return new CallResult(0, new CantConnectError() {Message = "Ping failed: " + e.Message}); + return new CallResult(0, new CantConnectError {Message = "Ping failed: " + e.Message}); if (e.InnerException is SocketException exception) - return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + exception.SocketErrorCode }); - return new CallResult(0, new CantConnectError() { Message = "Ping failed: " + e.InnerException.Message }); + return new CallResult(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode }); + return new CallResult(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message }); + } + finally + { + ctRegistration.Dispose(); + ping.Dispose(); } - return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError() { Message = "Ping failed: " + reply.Status }); + if(ct.IsCancellationRequested) + return new CallResult(0, new CancellationRequestedError()); + + return reply.Status == IPStatus.Success ? new CallResult(reply.RoundtripTime, null) : new CallResult(0, new CantConnectError { Message = "Ping failed: " + reply.Status }); } /// @@ -137,11 +150,14 @@ namespace CryptoExchange.Net /// The expected result type /// The uri to send the request to /// The method of the request + /// Cancellation token /// The parameters of the request /// Whether or not the request should be authenticated /// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) /// - protected virtual async Task> ExecuteRequest(Uri uri, string method = Constants.GetMethod, Dictionary parameters = null, bool signed = false, bool checkResult = true) where T : class + [return: NotNull] + protected virtual async Task> SendRequest(Uri uri, HttpMethod method, CancellationToken cancellationToken, + Dictionary? parameters = null, bool signed = false, bool checkResult = true) where T : class { log.Write(LogVerbosity.Debug, "Creating request for " + uri); if (signed && authProvider == null) @@ -151,13 +167,6 @@ namespace CryptoExchange.Net } var request = ConstructRequest(uri, method, parameters, signed); - - if (apiProxy != null) - { - log.Write(LogVerbosity.Debug, "Setting proxy"); - request.SetProxy(apiProxy.Host, apiProxy.Port, apiProxy.Login, apiProxy.Password); - } - foreach (var limiter in RateLimiters) { var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour); @@ -171,37 +180,67 @@ namespace CryptoExchange.Net log.Write(LogVerbosity.Debug, $"Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); } - string paramString = null; - if (parameters != null && method == Constants.PostMethod) - paramString = "with request body " + request.Content; + string? paramString = null; + if (method == HttpMethod.Post) + paramString = " with request body " + request.Content; - log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri} {paramString ?? ""}"); - var result = await ExecuteRequest(request).ConfigureAwait(false); - if(!result.Success) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, result.Error); - - var jsonResult = ValidateJson(result.Data); - if(!jsonResult.Success) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, jsonResult.Error); - - if (IsErrorResponse(jsonResult.Data)) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, ParseErrorResponse(jsonResult.Data)); - - var desResult = Deserialize(jsonResult.Data, checkResult); - if (!desResult.Success) - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, null, desResult.Error); - - return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, desResult.Data, null); + log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null? "": $" via proxy {apiProxy.Host}")}"); + return await GetResponse(request, cancellationToken).ConfigureAwait(false); } /// - /// Can be overridden to indicate if a response is an error response + /// Executes the request and returns the string result /// - /// The received data - /// True if error response - protected virtual bool IsErrorResponse(JToken data) + /// The request object to execute + /// Cancellation token + /// + private async Task> GetResponse(IRequest request, CancellationToken cancellationToken) { - return false; + try + { + TotalRequestsMade++; + var response = await request.GetResponse(cancellationToken).ConfigureAwait(false); + var statusCode = response.StatusCode; + var headers = response.ResponseHeaders; + var responseStream = await response.GetResponseStream().ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + var desResult = await Deserialize(responseStream).ConfigureAwait(false); + responseStream.Close(); + response.Close(); + + return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error); + } + else + { + using var reader = new StreamReader(responseStream); + var data = await reader.ReadToEndAsync().ConfigureAwait(false); + responseStream.Close(); + response.Close(); + var parseResult = ValidateJson(data); + return new WebCallResult(statusCode, headers, default, parseResult.Success ? ParseErrorResponse(parseResult.Data) :new ServerError(data)); + } + } + catch (HttpRequestException requestException) + { + log.Write(LogVerbosity.Warning, "Request exception: " + requestException.Message); + return new WebCallResult(null, null, default, new ServerError(requestException.Message)); + } + catch (TaskCanceledException canceledException) + { + if(canceledException.CancellationToken == cancellationToken) + { + // Cancellation token cancelled + log.Write(LogVerbosity.Warning, "Request cancel requested"); + return new WebCallResult(null, null, default, new CancellationRequestedError()); + } + else + { + // Request timed out + log.Write(LogVerbosity.Warning, "Request timed out"); + return new WebCallResult(null, null, default, new WebError("Request timed out")); + } + } } /// @@ -212,7 +251,7 @@ namespace CryptoExchange.Net /// The parameters of the request /// Whether or not the request should be authenticated /// - protected virtual IRequest ConstructRequest(Uri uri, string method, Dictionary parameters, bool signed) + protected virtual IRequest ConstructRequest(Uri uri, HttpMethod method, Dictionary? parameters, bool signed) { if (parameters == null) parameters = new Dictionary(); @@ -221,134 +260,53 @@ namespace CryptoExchange.Net if(authProvider != null) parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed); - if((method == Constants.GetMethod || method == Constants.DeleteMethod || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true) - uriString += "?" + parameters.CreateParamString(true); - - var request = RequestFactory.Create(uriString); - request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + if((method == HttpMethod.Get || method == HttpMethod.Delete || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true) + uriString += "?" + parameters.CreateParamString(true, arraySerialization); + + var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; + var request = RequestFactory.Create(method, uriString); request.Accept = Constants.JsonContentHeader; - request.Method = method; var headers = new Dictionary(); if (authProvider != null) - headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters, signed); + headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed); foreach (var header in headers) - request.Headers.Add(header.Key, header.Value); + request.AddHeader(header.Key, header.Value); - if ((method == Constants.PostMethod || method == Constants.PutMethod) && postParametersPosition != PostParameters.InUri) + if ((method == HttpMethod.Post || method == HttpMethod.Put) && postParametersPosition != PostParameters.InUri) { if(parameters?.Any() == true) - WriteParamBody(request, parameters); - else - WriteParamBody(request, "{}"); + WriteParamBody(request, parameters, contentType); + else + request.SetContent("{}", contentType); } return request; } - /// - /// Writes the string data of the parameters to the request body stream - /// - /// - /// - protected virtual void WriteParamBody(IRequest request, string stringData) - { - var data = Encoding.UTF8.GetBytes(stringData); - request.ContentLength = data.Length; - request.Content = stringData; - using (var stream = request.GetRequestStream().Result) - stream.Write(data, 0, data.Length); - } - /// /// Writes the parameters of the request to the request object, either in the query string or the request body /// /// /// - protected virtual void WriteParamBody(IRequest request, Dictionary parameters) + /// + protected virtual void WriteParamBody(IRequest request, Dictionary parameters, string contentType) { if (requestBodyFormat == RequestBodyFormat.Json) { var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value)); - WriteParamBody(request, stringData); + request.SetContent(stringData, contentType); } else if(requestBodyFormat == RequestBodyFormat.FormData) { - var formData = HttpUtility.ParseQueryString(String.Empty); + var formData = HttpUtility.ParseQueryString(string.Empty); foreach (var kvp in parameters.OrderBy(p => p.Key)) formData.Add(kvp.Key, kvp.Value.ToString()); var stringData = formData.ToString(); - WriteParamBody(request, stringData); + request.SetContent(stringData, contentType); } - } - - /// - /// Executes the request and returns the string result - /// - /// The request object to execute - /// - private async Task> ExecuteRequest(IRequest request) - { - var returnedData = ""; - try - { - request.Timeout = RequestTimeout; - TotalRequestsMade++; - var response = await request.GetResponse().ConfigureAwait(false); - using (var reader = new StreamReader(response.GetResponseStream())) - { - returnedData = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Debug, "Data returned: " + returnedData); - } - - var statusCode = response.StatusCode; - var returnHeaders = response.GetResponseHeaders(); - response.Close(); - return new WebCallResult(statusCode, returnHeaders, returnedData, null); - } - catch (WebException we) - { - var response = (HttpWebResponse)we.Response; - var statusCode = response?.StatusCode; - var returnHeaders = response?.Headers.ToIEnumerable(); - - try - { - using (var reader = new StreamReader(response.GetResponseStream())) - { - returnedData = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Warning, "Server returned an error: " + returnedData); - } - - response.Close(); - - var jsonResult = ValidateJson(returnedData); - return !jsonResult.Success ? new WebCallResult(statusCode, returnHeaders, null, jsonResult.Error) : new WebCallResult(statusCode, returnHeaders, null, ParseErrorResponse(jsonResult.Data)); - } - catch (Exception) - { - } - - var infoMessage = "No response from server"; - if (response == null) - { - infoMessage += $" | {we.Status} - {we.Message}"; - log.Write(LogVerbosity.Warning, infoMessage); - return new WebCallResult(0, null, null, new WebError(infoMessage)); - } - - infoMessage = $"Status: {response.StatusCode}-{response.StatusDescription}, Message: {we.Message}"; - log.Write(LogVerbosity.Warning, infoMessage); - response.Close(); - return new WebCallResult(statusCode, returnHeaders, null, new ServerError(infoMessage)); - } - catch (Exception e) - { - log.Write(LogVerbosity.Error, $"Unknown error occured: {e.GetType()}, {e.Message}, {e.StackTrace}"); - return new WebCallResult(null, null, null, new UnknownError(e.Message + ", data: " + returnedData)); - } - } + } /// /// Parse an error response from the server. Only used when server returns a status other than Success(200) diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index cc53894..77ea973 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -33,13 +34,13 @@ namespace CryptoExchange.Net protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); /// - public TimeSpan ReconnectInterval { get; private set; } + public TimeSpan ReconnectInterval { get; } /// - public bool AutoReconnect { get; private set; } + public bool AutoReconnect { get; } /// - public TimeSpan ResponseTimeout { get; private set; } + public TimeSpan ResponseTimeout { get; } /// - public TimeSpan SocketNoDataTimeout { get; private set; } + public TimeSpan SocketNoDataTimeout { get; } /// /// The max amount of concurrent socket connections /// @@ -50,11 +51,11 @@ namespace CryptoExchange.Net /// /// Handler for byte data /// - protected Func dataInterpreterBytes; + protected Func? dataInterpreterBytes; /// /// Handler for string data /// - protected Func dataInterpreterString; + protected Func? dataInterpreterString; /// /// Generic handlers /// @@ -62,11 +63,11 @@ namespace CryptoExchange.Net /// /// Periodic task /// - protected Task periodicTask; + protected Task? periodicTask; /// /// Periodic task event /// - protected AutoResetEvent periodicEvent; + protected AutoResetEvent? periodicEvent; /// /// Is disposing /// @@ -84,17 +85,11 @@ namespace CryptoExchange.Net /// /// Client options /// Authentication provider - protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider) + protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeOptions, authenticationProvider) { - Configure(exchangeOptions); - } + if (exchangeOptions == null) + throw new ArgumentNullException(nameof(exchangeOptions)); - /// - /// Configure the client using the provided options - /// - /// Options - protected void Configure(SocketClientOptions exchangeOptions) - { AutoReconnect = exchangeOptions.AutoReconnect; ReconnectInterval = exchangeOptions.ReconnectInterval; ResponseTimeout = exchangeOptions.SocketResponseTimeout; @@ -107,7 +102,7 @@ namespace CryptoExchange.Net /// /// Handler for byte data /// Handler for string data - protected void SetDataInterpreter(Func byteHandler, Func stringHandler) + protected void SetDataInterpreter(Func? byteHandler, Func? stringHandler) { dataInterpreterBytes = byteHandler; dataInterpreterString = stringHandler; @@ -122,7 +117,7 @@ namespace CryptoExchange.Net /// If the subscription should be authenticated /// The handler of update data /// - protected virtual Task> Subscribe(object request, string identifier, bool authenticated, Action dataHandler) + protected virtual Task> Subscribe(object? request, string? identifier, bool authenticated, Action dataHandler) { return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler); } @@ -137,11 +132,11 @@ namespace CryptoExchange.Net /// If the subscription should be authenticated /// The handler of update data /// - protected virtual async Task> Subscribe(string url, object request, string identifier, bool authenticated, Action dataHandler) + protected virtual async Task> Subscribe(string url, object? request, string? identifier, bool authenticated, Action dataHandler) { SocketConnection socket; SocketSubscription handler; - bool released = false; + var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { @@ -155,7 +150,7 @@ namespace CryptoExchange.Net } var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); - if (!connectResult.Success) + if (!connectResult) return new CallResult(null, connectResult.Error); } finally @@ -166,20 +161,26 @@ namespace CryptoExchange.Net semaphoreSlim.Release(); } + if (socket.PausedActivity) + { + log.Write(LogVerbosity.Info, "Socket has been paused, can't subscribe at this moment"); + return new CallResult(default, new ServerError("Socket is paused")); + } if (request != null) { var subResult = await SubscribeAndWait(socket, request, handler).ConfigureAwait(false); - if (!subResult.Success) + if (!subResult) { await socket.Close(handler).ConfigureAwait(false); return new CallResult(null, subResult.Error); } - } else + { handler.Confirmed = true; - + } + socket.ShouldReconnect = true; return new CallResult(new UpdateSubscription(socket, handler), null); } @@ -193,14 +194,8 @@ namespace CryptoExchange.Net /// protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription) { - CallResult callResult = null; - await socket.SendAndWait(request, ResponseTimeout, (data) => - { - if (!HandleSubscriptionResponse(socket, subscription, request, data, out callResult)) - return false; - - return true; - }).ConfigureAwait(false); + CallResult? callResult = null; + await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -211,7 +206,7 @@ namespace CryptoExchange.Net /// /// Query for data /// - /// Exepected result type + /// Expected result type /// The request to send /// Whether the socket should be authenticated /// @@ -231,7 +226,7 @@ namespace CryptoExchange.Net protected virtual async Task> Query(string url, object request, bool authenticated) { SocketConnection socket; - bool released = false; + var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { @@ -244,8 +239,8 @@ namespace CryptoExchange.Net } var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); - if (!connectResult.Success) - return new CallResult(default(T), connectResult.Error); + if (!connectResult) + return new CallResult(default, connectResult.Error); } finally { @@ -258,7 +253,7 @@ namespace CryptoExchange.Net if (socket.PausedActivity) { log.Write(LogVerbosity.Info, "Socket has been paused, can't send query at this moment"); - return new CallResult(default(T), new ServerError("Socket is paused")); + return new CallResult(default, new ServerError("Socket is paused")); } return await QueryAndWait(socket, request).ConfigureAwait(false); @@ -273,8 +268,8 @@ namespace CryptoExchange.Net /// protected virtual async Task> QueryAndWait(SocketConnection socket, object request) { - CallResult dataResult = new CallResult(default(T), new ServerError("No response on query received")); - await socket.SendAndWait(request, ResponseTimeout, (data) => + var dataResult = new CallResult(default, new ServerError("No response on query received")); + await socket.SendAndWait(request, ResponseTimeout, data => { if (!HandleQueryResponse(socket, request, data, out var callResult)) return false; @@ -294,28 +289,25 @@ namespace CryptoExchange.Net /// protected virtual async Task> ConnectIfNeeded(SocketConnection socket, bool authenticated) { - if (!socket.Connected) + if (socket.Connected) + return new CallResult(true, null); + + var connectResult = await ConnectSocket(socket).ConfigureAwait(false); + if (!connectResult) + return new CallResult(false, new CantConnectError()); + + if (!authenticated || socket.Authenticated) + return new CallResult(true, null); + + var result = await AuthenticateSocket(socket).ConfigureAwait(false); + if (!result) { - var connectResult = await ConnectSocket(socket).ConfigureAwait(false); - if (!connectResult.Success) - { - return new CallResult(false, new CantConnectError()); - } - - if (authenticated && !socket.Authenticated) - { - var result = await AuthenticateSocket(socket).ConfigureAwait(false); - if (!result.Success) - { - log.Write(LogVerbosity.Warning, "Socket authentication failed"); - result.Error.Message = "Authentication failed: " + result.Error.Message; - return new CallResult(false, result.Error); - } - - socket.Authenticated = true; - } + log.Write(LogVerbosity.Warning, "Socket authentication failed"); + result.Error!.Message = "Authentication failed: " + result.Error.Message; + return new CallResult(false, result.Error); } + socket.Authenticated = true; return new CallResult(true, null); } @@ -328,7 +320,7 @@ namespace CryptoExchange.Net /// The message /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the query - protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult); + protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, [NotNullWhen(true)]out CallResult? callResult); /// /// Needs to check if a received message was an answer to a subscription request (preferable by id) and set the callResult out to whatever the response is /// @@ -338,7 +330,7 @@ namespace CryptoExchange.Net /// The message /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the subscription request - protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult callResult); + protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult? callResult); /// /// Needs to check if a received message matches a handler. Typically if an update message matches the request /// @@ -387,29 +379,31 @@ namespace CryptoExchange.Net /// The socket connection the handler is on /// The handler of the data received /// - protected virtual SocketSubscription AddHandler(object request, string identifier, bool userSubscription, SocketConnection connection, Action dataHandler) + protected virtual SocketSubscription AddHandler(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action dataHandler) { - Action internalHandler = (socketWrapper, data) => + void InternalHandler(SocketConnection socketWrapper, JToken data) { if (typeof(T) == typeof(string)) { - dataHandler((T)Convert.ChangeType(data.ToString(), typeof(T))); + dataHandler((T) Convert.ChangeType(data.ToString(), typeof(T))); return; } var desResult = Deserialize(data, false); - if (!desResult.Success) + if (!desResult) { log.Write(LogVerbosity.Warning, $"Failed to deserialize data into type {typeof(T)}: {desResult.Error}"); return; } dataHandler(desResult.Data); - }; + } - if (request != null) - return connection.AddHandler(request, userSubscription, internalHandler); - return connection.AddHandler(identifier, userSubscription, internalHandler); + var handler = request == null + ? SocketSubscription.CreateForIdentifier(identifier!, userSubscription, InternalHandler) + : SocketSubscription.CreateForRequest(request, userSubscription, InternalHandler); + connection.AddHandler(handler); + return handler; } /// @@ -417,11 +411,12 @@ namespace CryptoExchange.Net /// /// The name of the request handler. Needs to be unique /// The action to execute when receiving a message for this handler (checked by ) - protected virtual void AddGenericHandler(string identifier, Action action) + protected void AddGenericHandler(string identifier, Action action) { genericHandlers.Add(identifier, action); + var handler = SocketSubscription.CreateForIdentifier(identifier, false, action); foreach (var connection in sockets.Values) - connection.AddHandler(identifier, false, action); + connection.AddHandler(handler); } /// @@ -447,7 +442,11 @@ namespace CryptoExchange.Net var socket = CreateSocket(address); var socketWrapper = new SocketConnection(this, socket); foreach (var kvp in genericHandlers) - socketWrapper.AddHandler(kvp.Key, false, kvp.Value); + { + var handler = SocketSubscription.CreateForIdentifier(kvp.Key, false, kvp.Value); + socketWrapper.AddHandler(handler); + } + return socketWrapper; } @@ -486,7 +485,7 @@ namespace CryptoExchange.Net socket.DataInterpreterString = dataInterpreterString; socket.OnError += e => { - log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e.ToString()); + log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e); }; return socket; } @@ -498,6 +497,9 @@ namespace CryptoExchange.Net /// Method returning the object to send public virtual void SendPeriodic(TimeSpan interval, Func objGetter) { + if (objGetter == null) + throw new ArgumentNullException(nameof(objGetter)); + periodicEvent = new AutoResetEvent(false); periodicTask = Task.Run(async () => { @@ -516,20 +518,19 @@ namespace CryptoExchange.Net break; var obj = objGetter(socket); - if (obj != null) + if (obj == null) + continue; + + try { - try - { - socket.Send(obj); - } - catch (Exception ex) - { - log.Write(LogVerbosity.Warning, "Periodic send failed: " + ex); - } + socket.Send(obj); + } + catch (Exception ex) + { + log.Write(LogVerbosity.Warning, "Periodic send failed: " + ex); } } } - }); } @@ -542,7 +543,7 @@ namespace CryptoExchange.Net public virtual async Task Unsubscribe(UpdateSubscription subscription) { if (subscription == null) - return; + throw new ArgumentNullException(nameof(subscription)); log.Write(LogVerbosity.Info, "Closing subscription"); await subscription.Close().ConfigureAwait(false); @@ -554,9 +555,9 @@ namespace CryptoExchange.Net /// public virtual async Task UnsubscribeAll() { - log.Write(LogVerbosity.Debug, $"Closing all {sockets.Count} subscriptions"); + log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.HandlerCount)} subscriptions"); - await Task.Run(() => + await Task.Run(async () => { var tasks = new List(); { @@ -565,7 +566,7 @@ namespace CryptoExchange.Net tasks.Add(sub.Close()); } - Task.WaitAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); }).ConfigureAwait(false); } @@ -576,9 +577,10 @@ namespace CryptoExchange.Net { disposing = true; periodicEvent?.Set(); + periodicEvent?.Dispose(); log.Write(LogVerbosity.Debug, "Disposing socket client, closing all subscriptions"); UnsubscribeAll().Wait(); - + semaphoreSlim?.Dispose(); base.Dispose(); } } diff --git a/CryptoExchange.Net/Sockets/BaseSocket.cs b/CryptoExchange.Net/Sockets/BaseSocket.cs index fee144a..a69be1f 100644 --- a/CryptoExchange.Net/Sockets/BaseSocket.cs +++ b/CryptoExchange.Net/Sockets/BaseSocket.cs @@ -21,52 +21,106 @@ namespace CryptoExchange.Net.Sockets internal static int lastStreamId; private static readonly object streamIdLock = new object(); - protected WebSocket socket; + /// + /// Socket + /// + protected WebSocket? socket; + /// + /// Log + /// protected Log log; - protected object socketLock = new object(); + private readonly object socketLock = new object(); + /// + /// Error handlers + /// protected readonly List> errorHandlers = new List>(); + /// + /// Open handlers + /// protected readonly List openHandlers = new List(); + /// + /// Close handlers + /// protected readonly List closeHandlers = new List(); + /// + /// Message handlers + /// protected readonly List> messageHandlers = new List>(); - protected IDictionary cookies; - protected IDictionary headers; - protected HttpConnectProxy proxy; + private readonly IDictionary cookies; + private readonly IDictionary headers; + private HttpConnectProxy? proxy; + /// + /// Id + /// public int Id { get; } + /// + /// If is reconnecting + /// public bool Reconnecting { get; set; } - public string Origin { get; set; } + /// + /// Origin + /// + public string? Origin { get; set; } + /// + /// Url + /// public string Url { get; } - public bool IsClosed => socket.State == WebSocketState.Closed; - public bool IsOpen => socket.State == WebSocketState.Open; + /// + /// Is closed + /// + public bool IsClosed => socket?.State == null || socket.State == WebSocketState.Closed; + /// + /// Is open + /// + public bool IsOpen => socket?.State == WebSocketState.Open; + /// + /// Protocols + /// public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; - public Func DataInterpreterBytes { get; set; } - public Func DataInterpreterString { get; set; } + /// + /// Interpreter for bytes + /// + public Func? DataInterpreterBytes { get; set; } + /// + /// Interpreter for strings + /// + public Func? DataInterpreterString { get; set; } + /// + /// Last action time + /// public DateTime LastActionTime { get; private set; } + /// + /// Timeout + /// public TimeSpan Timeout { get; set; } - private Task timeoutTask; - - public bool PingConnection - { - get => socket.EnableAutoSendPing; - set => socket.EnableAutoSendPing = value; - } - - public TimeSpan PingInterval - { - get => TimeSpan.FromSeconds(socket.AutoSendPingInterval); - set => socket.AutoSendPingInterval = (int) Math.Round(value.TotalSeconds); - } + private Task? timeoutTask; + /// + /// Socket state + /// public WebSocketState SocketState => socket?.State ?? WebSocketState.None; + /// + /// ctor + /// + /// + /// public BaseSocket(Log log, string url):this(log, url, new Dictionary(), new Dictionary()) { } + /// + /// ctor + /// + /// + /// + /// + /// public BaseSocket(Log log, string url, IDictionary cookies, IDictionary headers) { Id = NextStreamId(); @@ -106,27 +160,43 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// On close + /// public event Action OnClose { add => closeHandlers.Add(value); remove => closeHandlers.Remove(value); } + /// + /// On message + /// public event Action OnMessage { add => messageHandlers.Add(value); remove => messageHandlers.Remove(value); } + /// + /// On error + /// public event Action OnError { add => errorHandlers.Add(value); remove => errorHandlers.Remove(value); } + /// + /// On open + /// public event Action OnOpen { add => openHandlers.Add(value); remove => openHandlers.Remove(value); } + /// + /// Handle + /// + /// protected void Handle(List handlers) { LastActionTime = DateTime.UtcNow; @@ -134,6 +204,12 @@ namespace CryptoExchange.Net.Sockets handle?.Invoke(); } + /// + /// Handle + /// + /// + /// + /// protected void Handle(List> handlers, T data) { LastActionTime = DateTime.UtcNow; @@ -141,6 +217,10 @@ namespace CryptoExchange.Net.Sockets handle?.Invoke(data); } + /// + /// Checks if timed out + /// + /// protected async Task CheckTimeout() { while (true) @@ -153,7 +233,7 @@ namespace CryptoExchange.Net.Sockets if (DateTime.UtcNow - LastActionTime > Timeout) { log.Write(LogVerbosity.Warning, $"No data received for {Timeout}, reconnecting socket"); - Close().ConfigureAwait(false); + _ = Close().ConfigureAwait(false); return; } } @@ -162,6 +242,10 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Close socket + /// + /// public virtual async Task Close() { await Task.Run(() => @@ -176,7 +260,7 @@ namespace CryptoExchange.Net.Sockets var waitLock = new object(); log?.Write(LogVerbosity.Debug, $"Socket {Id} closing"); - var evnt = new ManualResetEvent(false); + ManualResetEvent? evnt = new ManualResetEvent(false); var handler = new EventHandler((o, a) => { lock(waitLock) @@ -196,6 +280,9 @@ namespace CryptoExchange.Net.Sockets }).ConfigureAwait(false); } + /// + /// Reset socket + /// public virtual void Reset() { lock (socketLock) @@ -206,11 +293,19 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Send data + /// + /// public virtual void Send(string data) { - socket.Send(data); + socket?.Send(data); } + /// + /// Connect socket + /// + /// public virtual Task Connect() { if (socket == null) @@ -239,7 +334,7 @@ namespace CryptoExchange.Net.Sockets { log?.Write(LogVerbosity.Debug, $"Socket {Id} connecting"); var waitLock = new object(); - var evnt = new ManualResetEvent(false); + ManualResetEvent? evnt = new ManualResetEvent(false); var handler = new EventHandler((o, a) => { lock (waitLock) @@ -267,11 +362,13 @@ namespace CryptoExchange.Net.Sockets if (connected) { log?.Write(LogVerbosity.Debug, $"Socket {Id} connected"); - if ((timeoutTask == null || timeoutTask.IsCompleted) && Timeout != default(TimeSpan)) + if ((timeoutTask == null || timeoutTask.IsCompleted) && Timeout != default) timeoutTask = Task.Run(CheckTimeout); } else + { log?.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State); + } } if (socket.State == WebSocketState.Connecting) @@ -281,6 +378,11 @@ namespace CryptoExchange.Net.Sockets }); } + /// + /// Set a proxy + /// + /// + /// public virtual void SetProxy(string host, int port) { proxy = IPAddress.TryParse(host, out var address) @@ -288,6 +390,9 @@ namespace CryptoExchange.Net.Sockets : new HttpConnectProxy(new DnsEndPoint(host, port)); } + /// + /// Dispose + /// public void Dispose() { lock (socketLock) diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 2bb0df3..8b6e2c8 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -19,15 +19,23 @@ namespace CryptoExchange.Net.Sockets /// /// Connection lost event /// - public event Action ConnectionLost; + public event Action? ConnectionLost; /// /// Connecting restored event /// - public event Action ConnectionRestored; + public event Action? ConnectionRestored; + /// + /// The connection is paused event + /// + public event Action? ActivityPaused; + /// + /// The connection is unpaused event + /// + public event Action? ActivityUnpaused; /// /// Connecting closed event /// - public event Action Closed; + public event Action? Closed; /// /// The amount of handlers @@ -60,12 +68,27 @@ namespace CryptoExchange.Net.Sockets /// Time of disconnecting /// public DateTime? DisconnectTime { get; set; } + /// /// If activity is paused /// - public bool PausedActivity { get; set; } + public bool PausedActivity + { + get => pausedActivity; + set + { + if (pausedActivity != value) + { + pausedActivity = value; + log.Write(LogVerbosity.Debug, "Paused activity: " + value); + if(pausedActivity) ActivityPaused?.Invoke(); + else ActivityUnpaused?.Invoke(); + } + } + } - internal readonly List handlers; + private bool pausedActivity; + private readonly List handlers; private readonly object handlersLock = new object(); private bool lostTriggered; @@ -109,38 +132,7 @@ namespace CryptoExchange.Net.Sockets Connected = true; }; } - - /// - /// Add a handler - /// - /// The request object - /// If it is a user subscription or a generic handler - /// The data handler - /// - public SocketSubscription AddHandler(object request, bool userSubscription, Action dataHandler) - { - var handler = new SocketSubscription(null, request, userSubscription, dataHandler); - lock (handlersLock) - handlers.Add(handler); - return handler; - } - - /// - /// Add a handler - /// - /// The identifier of the handler - /// If it is a user subscription or a generic handler - /// The data handler - /// - /// - public SocketSubscription AddHandler(string identifier, bool userSubscription, Action dataHandler) - { - var handler = new SocketSubscription(identifier, null, userSubscription, dataHandler); - lock (handlersLock) - handlers.Add(handler); - return handler; - } - + private void ProcessMessage(string data) { log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data); @@ -167,21 +159,31 @@ namespace CryptoExchange.Net.Sockets } } + /// + /// Add handler + /// + /// + public void AddHandler(SocketSubscription handler) + { + lock(handlersLock) + handlers.Add(handler); + } + private bool HandleData(JToken tokenData) { - SocketSubscription currentSubscription = null; + SocketSubscription? currentSubscription = null; try { - bool handled = false; + var handled = false; var sw = Stopwatch.StartNew(); lock (handlersLock) { - foreach (var handler in handlers) + foreach (var handler in handlers.ToList()) { currentSubscription = handler; if (handler.Request == null) { - if (socketClient.MessageMatchesHandler(tokenData, handler.Identifier)) + if (socketClient.MessageMatchesHandler(tokenData, handler.Identifier!)) { handled = true; handler.MessageHandler(this, tokenData); @@ -237,7 +239,10 @@ namespace CryptoExchange.Net.Sockets /// How null values should be serialized public virtual void Send(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore) { - Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling })); + if(obj is string str) + Send(str); + else + Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling })); } /// @@ -326,7 +331,7 @@ namespace CryptoExchange.Net.Sockets if (Authenticated) { var authResult = await socketClient.AuthenticateSocket(this).ConfigureAwait(false); - if (!authResult.Success) + if (!authResult) { log.Write(LogVerbosity.Info, "Authentication failed on reconnected socket. Disconnecting and reconnecting."); return false; @@ -343,9 +348,9 @@ namespace CryptoExchange.Net.Sockets var taskList = new List(); foreach (var handler in handlerList) { - var task = socketClient.SubscribeAndWait(this, handler.Request, handler).ContinueWith(t => + var task = socketClient.SubscribeAndWait(this, handler.Request!, handler).ContinueWith(t => { - if (!t.Result.Success) + if (!t.Result) success = false; }); taskList.Add(task); @@ -387,7 +392,7 @@ namespace CryptoExchange.Net.Sockets if (subscription.Confirmed) await socketClient.Unsubscribe(this, subscription).ConfigureAwait(false); - bool shouldCloseWrapper = false; + var shouldCloseWrapper = false; lock (handlersLock) { handlers.Remove(subscription); @@ -403,7 +408,7 @@ namespace CryptoExchange.Net.Sockets internal class PendingRequest { public Func Handler { get; } - public JToken Result { get; private set; } + public JToken? Result { get; private set; } public ManualResetEvent Event { get; } public TimeSpan Timeout { get; } diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index 0de0546..492ab9d 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Sockets /// /// Exception event /// - public event Action Exception; + public event Action? Exception; /// /// Message handlers for this subscription. Should return true if the message is handled and should not be distributed to the other handlers @@ -21,11 +21,11 @@ namespace CryptoExchange.Net.Sockets /// /// Request object /// - public object Request { get; set; } + public object? Request { get; set; } /// /// Subscription identifier /// - public string Identifier { get; set; } + public string? Identifier { get; set; } /// /// Is user subscription or generic /// @@ -36,22 +36,40 @@ namespace CryptoExchange.Net.Sockets /// public bool Confirmed { get; set; } - - /// - /// ctor - /// - /// - /// - /// - /// - public SocketSubscription(string identifier, object request, bool userSubscription, Action dataHandler) + private SocketSubscription(object? request, string? identifier, bool userSubscription, Action dataHandler) { UserSubscription = userSubscription; MessageHandler = dataHandler; - Identifier = identifier; Request = request; + Identifier = identifier; } - + + /// + /// Create SocketSubscription for a request + /// + /// + /// + /// + /// + public static SocketSubscription CreateForRequest(object request, bool userSubscription, + Action dataHandler) + { + return new SocketSubscription(request, null, userSubscription, dataHandler); + } + + /// + /// Create SocketSubscription for an identifier + /// + /// + /// + /// + /// + public static SocketSubscription CreateForIdentifier(string identifier, bool userSubscription, + Action dataHandler) + { + return new SocketSubscription(null, identifier, userSubscription, dataHandler); + } + /// /// Invoke the exception event /// diff --git a/CryptoExchange.Net/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Sockets/UpdateSubscription.cs index 681b0c5..98f37b4 100644 --- a/CryptoExchange.Net/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Sockets/UpdateSubscription.cs @@ -29,6 +29,24 @@ namespace CryptoExchange.Net.Sockets remove => connection.ConnectionRestored -= value; } + /// + /// Event when the connection to the server is paused. No operations can be performed while paused + /// + public event Action ActivityPaused + { + add => connection.ActivityPaused += value; + remove => connection.ActivityPaused -= value; + } + + /// + /// Event when the connection to the server is unpaused + /// + public event Action ActivityUnpaused + { + add => connection.ActivityUnpaused += value; + remove => connection.ActivityUnpaused -= value; + } + /// /// Event when an exception happened /// diff --git a/README.md b/README.md index 760c1d5..f7b667c 100644 --- a/README.md +++ b/README.md @@ -14,30 +14,34 @@ A base library for easy implementation of cryptocurrency API's. Include: ## Implementations - - - - - - +
+
Bittrex
+
Bitfinex
+
Binance
+
CoinEx
+
Huobi
+
Kucoin
+
+Kraken +
@@ -59,7 +63,6 @@ Planned implementations (no timeline or specific order): * BitMEX * Bitstamp * CoinFalcon -* Kraken * Binance DEX ## Donations @@ -127,10 +130,10 @@ Note that when using a file it can provide credentials for multiple exchanges by ```` // File content: { - "binanceKey": "binanceApiKey", - "binanceSecret": "binanceApiSecret", - "bittrexKey": "bitrexApiKey", - "bittrexSecret": "bittrexApiSecret", + "binanceKey": "actualBinanceApiKey", + "binanceSecret": "actualBinanceApiSecret", + "bittrexKey": "actualBittrexApiKey", + "bittrexSecret": "actualBittrexApiSecret", } // Loading: @@ -142,7 +145,7 @@ using (var stream = File.OpenRead("/path/to/credential-file")) }); BittrexClient.SetDefaultOptions(new BittrexClientOptions { - ApiCredentials = new ApiCredentials(stream, "BittrexKey", "BittrexSecret") + ApiCredentials = new ApiCredentials(stream, "bittrexKey", "bittrexSecret") }); } ```` @@ -156,7 +159,7 @@ To unsubscribe use the client.Unsubscribe method and pass the UpdateSubscription ````C# // Subscribe var client = new BinanceSocketClient(); -var subResult = client.SubscribeToDepthStream("BTCUSDT", data => {}); +var subResult = client.SubscribeToOrderBookUpdates("BTCUSDT", data => {}); // Unsubscribe client.Unsubscribe(subResult.Data); @@ -169,6 +172,7 @@ but the implementation is similar for each library: ````C# var orderBook = new BinanceSymbolOrderBook("BTCUSDT", new BinanceOrderBookOptions(20)); orderBook.OnStatusChange += (oldStatus, newStatus) => Console.WriteLine($"Book state changed from {oldStatus} to {newStatus}"); +orderBook.OnOrderBookUpdate += (changedBids, changedAsks) => Console.WriteLine("Book updated"); var startResult = await orderBook.StartAsync(); if(!startResult.Success) { @@ -190,6 +194,36 @@ The order book will automatically reconnect when the connection is lost and resy To stop synchronizing an order book use the `Stop` method. ## Release notes +* Version 3.0.5 - 05 Feb 2020 + * Added PausedActivity events on socket subscriptions + +* Version 3.0.4 - 29 Jan 2020 + * Removed unnecessary json serialization + +* Version 3.0.3 - 23 Jan 2020 + * Added OnBestOffersChanged event to order book implementations + +* Version 3.0.2 - 10 Dec 2019 + * Removed invalid check for unauthenticated proxy + +* Version 3.0.1 - 14 Nov 2019 + * Re-enabled debug response logging + +* Version 3.0.0 - 23 Oct 2019 + * Updated to C# 8.0 + * Added .NetStandard2.1 support + * Added Nullability support + * Now using HttpClient instead of WebRequest, should result in faster consequtive requests + * Added CancellationToken support + * Added bool compare override to CallResult (now possible to `if(callresult)` instead of `if(callresult.Success)`) + * Added input validation methods + * Wrong input will now throw exceptions rather than error results + * OnOrderBookUpdate event added to `SymbolOrderBook` + + +* Version 2.1.8 - 29 Aug 2019 + * Added array serialization options for implementations + * Version 2.1.7 - 07 Aug 2019 * Fixed bug with socket connection not being disposed after lost connection * Resubscribing after reconnecting socket now in parallel