mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-08 16:36:15 +00:00
commit
c2885cbd67
@ -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
|
@ -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<TextWriter> { 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<TextWriter> { new StringWriter(stringBuilder) },
|
||||
LogVerbosity = verbosity
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
@ -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<IRateLimiter>{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<IRateLimiter> { 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<IRateLimiter> { 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<IRateLimiter> { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||
|
@ -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);
|
||||
|
@ -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<string, string> AddAuthenticationToHeaders(string uri, string method, Dictionary<string, object> parameters, bool signed)
|
||||
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
|
||||
{
|
||||
return base.AddAuthenticationToHeaders(uri, method, parameters, signed);
|
||||
}
|
||||
|
||||
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, string method, Dictionary<string, object> parameters, bool signed)
|
||||
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
|
||||
{
|
||||
return base.AddAuthenticationToParameters(uri, method, parameters, signed);
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -1,7 +1,4 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
|
@ -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<IRequestFactory>().Object;
|
||||
}
|
||||
@ -39,36 +39,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
responseStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
var response = new Mock<IResponse>();
|
||||
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<IRequest>();
|
||||
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<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<string>()))
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>()))
|
||||
.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<IRequest>();
|
||||
request.Setup(c => c.Headers).Returns(new WebHeaderCollection());
|
||||
request.Setup(c => c.GetResponse()).Throws(we);
|
||||
request.Setup(c => c.GetResponse(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<string>()))
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>()))
|
||||
.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<HttpWebResponse>();
|
||||
r.Setup(x => x.GetResponseStream()).Returns(responseStream);
|
||||
var we = new WebException("", null, WebExceptionStatus.Success, r.Object);
|
||||
var response = new Mock<IResponse>();
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||
response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var request = new Mock<IRequest>();
|
||||
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<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<string>()))
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(string method = "GET") where T:class
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||
{
|
||||
return await ExecuteRequest<T>(new Uri("http://www.test.com"), method);
|
||||
return await SendRequest<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult<object> callResult)
|
||||
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
|
||||
out CallResult<object> 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<CallResult<bool>> AuthenticateSocket(SocketConnection s)
|
||||
protected internal override Task<CallResult<bool>> AuthenticateSocket(SocketConnection s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<bool> Unsubscribe(SocketConnection connection, SocketSubscription s)
|
||||
protected internal override Task<bool> Unsubscribe(SocketConnection connection, SocketSubscription s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
1
CryptoExchange.Net/AssemblyInfo.cs
Normal file
1
CryptoExchange.Net/AssemblyInfo.cs
Normal file
@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
11
CryptoExchange.Net/Attributes/JsonConversionAttribute.cs
Normal file
11
CryptoExchange.Net/Attributes/JsonConversionAttribute.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for conversion in ArrayConverter
|
||||
/// </summary>
|
||||
public class JsonConversionAttribute: Attribute
|
||||
{
|
||||
}
|
||||
}
|
210
CryptoExchange.Net/Attributes/NullableAttributes.cs
Normal file
210
CryptoExchange.Net/Attributes/NullableAttributes.cs
Normal file
@ -0,0 +1,210 @@
|
||||
#if !NETSTANDARD2_1
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that <see langword="null"/> is allowed as an input even if the
|
||||
/// corresponding type disallows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property,
|
||||
Inherited = false
|
||||
)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class AllowNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AllowNullAttribute"/> class.
|
||||
/// </summary>
|
||||
public AllowNullAttribute() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that <see langword="null"/> is disallowed as an input even if the
|
||||
/// corresponding type allows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property,
|
||||
Inherited = false
|
||||
)]
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class DisallowNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DisallowNullAttribute"/> class.
|
||||
/// </summary>
|
||||
public DisallowNullAttribute() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that a method that will never return under any circumstance.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class DoesNotReturnAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DoesNotReturnAttribute"/> class.
|
||||
/// </summary>
|
||||
public DoesNotReturnAttribute() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the method will not return if the associated <see cref="Boolean"/>
|
||||
/// parameter is passed the specified value.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class DoesNotReturnIfAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the condition parameter value.
|
||||
/// Code after the method is considered unreachable by diagnostics if the argument
|
||||
/// to the associated parameter matches this value.
|
||||
/// </summary>
|
||||
public bool ParameterValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DoesNotReturnIfAttribute"/>
|
||||
/// class with the specified parameter value.
|
||||
/// </summary>
|
||||
/// <param name="parameterValue">
|
||||
/// The condition parameter value.
|
||||
/// Code after the method is considered unreachable by diagnostics if the argument
|
||||
/// to the associated parameter matches this value.
|
||||
/// </param>
|
||||
public DoesNotReturnIfAttribute(bool parameterValue)
|
||||
{
|
||||
ParameterValue = parameterValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that an output may be <see langword="null"/> even if the
|
||||
/// corresponding type disallows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Field | AttributeTargets.Parameter |
|
||||
AttributeTargets.Property | AttributeTargets.ReturnValue,
|
||||
Inherited = false
|
||||
)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class MaybeNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MaybeNullAttribute"/> class.
|
||||
/// </summary>
|
||||
public MaybeNullAttribute() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that when a method returns <see cref="ReturnValue"/>,
|
||||
/// the parameter may be <see langword="null"/> even if the corresponding type disallows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class MaybeNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the return value condition.
|
||||
/// If the method returns this value, the associated parameter may be <see langword="null"/>.
|
||||
/// </summary>
|
||||
public bool ReturnValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the attribute with the specified return value condition.
|
||||
/// </summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition.
|
||||
/// If the method returns this value, the associated parameter may be <see langword="null"/>.
|
||||
/// </param>
|
||||
public MaybeNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that an output is not <see langword="null"/> even if the
|
||||
/// corresponding type allows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Field | AttributeTargets.Parameter |
|
||||
AttributeTargets.Property | AttributeTargets.ReturnValue,
|
||||
Inherited = false
|
||||
)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class NotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotNullAttribute"/> class.
|
||||
/// </summary>
|
||||
public NotNullAttribute() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the output will be non-<see langword="null"/> if the
|
||||
/// named parameter is non-<see langword="null"/>.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue,
|
||||
AllowMultiple = true,
|
||||
Inherited = false
|
||||
)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class NotNullIfNotNullAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the associated parameter name.
|
||||
/// The output will be non-<see langword="null"/> if the argument to the
|
||||
/// parameter specified is non-<see langword="null"/>.
|
||||
/// </summary>
|
||||
public string ParameterName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the attribute with the associated parameter name.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">
|
||||
/// The associated parameter name.
|
||||
/// The output will be non-<see langword="null"/> if the argument to the
|
||||
/// parameter specified is non-<see langword="null"/>.
|
||||
/// </param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that when a method returns <see cref="ReturnValue"/>,
|
||||
/// the parameter will not be <see langword="null"/> even if the corresponding type allows it.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the return value condition.
|
||||
/// If the method returns this value, the associated parameter will not be <see langword="null"/>.
|
||||
/// </summary>
|
||||
public bool ReturnValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the attribute with the specified return value condition.
|
||||
/// </summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition.
|
||||
/// If the method returns this value, the associated parameter will not be <see langword="null"/>.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -14,17 +14,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// The api key to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString Key { get; private set; }
|
||||
public SecureString? Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString Secret { get; private set; }
|
||||
public SecureString? Secret { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key to authenticate requests
|
||||
/// </summary>
|
||||
public PrivateKey PrivateKey { get; }
|
||||
public PrivateKey? PrivateKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the credentials
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ApiCredentials Copy()
|
||||
{
|
||||
if (PrivateKey == null)
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||
else
|
||||
return new ApiCredentials(PrivateKey!.Copy());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -66,10 +78,10 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="inputStream">The stream containing the json data</param>
|
||||
/// <param name="identifierKey">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'.</param>
|
||||
/// <param name="identifierSecret">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'.</param>
|
||||
public ApiCredentials(Stream inputStream, string identifierKey = null, string identifierSecret = null)
|
||||
{
|
||||
using (var reader = new StreamReader(inputStream, Encoding.ASCII, false, 512, true))
|
||||
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)
|
||||
@ -81,9 +93,8 @@ namespace CryptoExchange.Net.Authentication
|
||||
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,27 +105,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="data"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
protected string TryGetValue(JToken data, string key)
|
||||
protected string? TryGetValue(JToken data, string key)
|
||||
{
|
||||
if (data[key] == null)
|
||||
return null;
|
||||
return (string) data[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a secure string from a string
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <returns></returns>
|
||||
protected SecureString CreateSecureString(string source)
|
||||
{
|
||||
var secureString = new SecureString();
|
||||
foreach (var c in source)
|
||||
secureString.AppendChar(c);
|
||||
secureString.MakeReadOnly();
|
||||
return secureString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
@ -29,7 +30,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="signed"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, string method, Dictionary<string, object> parameters, bool signed)
|
||||
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
|
||||
{
|
||||
return parameters;
|
||||
}
|
||||
@ -42,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="signed"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, string method, Dictionary<string, object> parameters, bool signed)
|
||||
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// The private key's pass phrase
|
||||
/// </summary>
|
||||
public SecureString Passphrase { get; }
|
||||
public SecureString? Passphrase { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the private key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PrivateKey Copy()
|
||||
{
|
||||
if (Passphrase == null)
|
||||
return new PrivateKey(Key.GetString());
|
||||
else
|
||||
return new PrivateKey(Key.GetString(), Passphrase.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
@ -7,8 +7,11 @@ 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
|
||||
{
|
||||
@ -20,7 +23,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// The address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress { get; private set; }
|
||||
public string BaseAddress { get; }
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
@ -28,11 +31,11 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// The api proxy
|
||||
/// </summary>
|
||||
protected ApiProxy apiProxy;
|
||||
protected ApiProxy? apiProxy;
|
||||
/// <summary>
|
||||
/// The auth provider
|
||||
/// </summary>
|
||||
protected internal AuthenticationProvider authProvider;
|
||||
protected internal AuthenticationProvider? authProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The last used id
|
||||
@ -59,26 +62,17 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="authenticationProvider"></param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Configure the client using the provided options
|
||||
/// </summary>
|
||||
/// <param name="clientOptions">Options</param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<JToken>(null, new DeserializeError(info));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<JToken>(null, new DeserializeError(info));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize Unknown Exception: {ex.Message}. Data: {data}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<JToken>(null, new DeserializeError(info));
|
||||
}
|
||||
}
|
||||
@ -137,10 +128,16 @@ namespace CryptoExchange.Net
|
||||
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer serializer = null)
|
||||
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer? serializer = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
return !tokenResult.Success ? new CallResult<T>(default(T), tokenResult.Error) : Deserialize<T>(tokenResult.Data, checkObject, serializer);
|
||||
if (!tokenResult)
|
||||
{
|
||||
log.Write(LogVerbosity.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(default, tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, checkObject, serializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -151,7 +148,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, bool checkObject = true, JsonSerializer serializer = null)
|
||||
protected CallResult<T> Deserialize<T>(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<T>(default(T), new DeserializeError(info));
|
||||
return new CallResult<T>(default, new DeserializeError(info));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {obj}";
|
||||
log.Write(LogVerbosity.Error, info);
|
||||
return new CallResult<T>(default(T), new DeserializeError(info));
|
||||
return new CallResult<T>(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<T>(default(T), new DeserializeError(info));
|
||||
return new CallResult<T>(default, new DeserializeError(info));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> Deserialize<T>(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<T>(JsonConvert.DeserializeObject<T>(data), null);
|
||||
}
|
||||
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
return new CallResult<T>(serializer.Deserialize<T>(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<T>(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<T>(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<T>(default, new DeserializeError(data));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> 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<JsonConverterAttribute>(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)
|
||||
{
|
||||
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<PropertyInfo> props)
|
||||
private static PropertyInfo? GetProperty(string name, IEnumerable<PropertyInfo> 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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// Base class for enum converters
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of enum to convert</typeparam>
|
||||
public abstract class BaseConverter<T>: JsonConverter
|
||||
public abstract class BaseConverter<T>: JsonConverter where T: struct
|
||||
{
|
||||
/// <summary>
|
||||
/// The enum->string mapping
|
||||
@ -38,7 +38,7 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
@ -8,6 +8,8 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
public class TimestampNanoSecondsConverter : JsonConverter
|
||||
{
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
@ -15,12 +17,11 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,11 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
|
||||
|
@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
@ -1,19 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<PackageVersion>2.1.7</PackageVersion>
|
||||
<Description>A base package for implementing cryptocurrency exchange API's</Description>
|
||||
<PackageVersion>3.0.5</PackageVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/JKorf/CryptoExchange.Net/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>2.1.7 - Fixed bug with socket connection not being disposed after lost connection, Resubscribing after reconnecting socket now in parallel</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>3.0.5 - Added PausedActivity events on socket subscriptions</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object value)
|
||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
||||
{
|
||||
if(value != null)
|
||||
parameters.Add(key, value);
|
||||
@ -58,7 +59,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string value)
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
@ -69,14 +70,22 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="parameters">The parameters to use</param>
|
||||
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
||||
/// <param name="serializationType">How to serialize array parameters</param>
|
||||
/// <returns></returns>
|
||||
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues)
|
||||
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||
{
|
||||
var uriString = "";
|
||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||
foreach (var arrayEntry in arraysParameters)
|
||||
{
|
||||
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<object>().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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Header collection to inenumerable
|
||||
/// Create a secure string from a string
|
||||
/// </summary>
|
||||
/// <param name="headers"></param>
|
||||
/// <param name="source"></param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<Tuple<string, string>> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -143,8 +149,8 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public static async Task<bool> 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<bool>();
|
||||
@ -183,7 +189,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="stringData"></param>
|
||||
/// <param name="log"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an int is one of the allowed values
|
||||
/// </summary>
|
||||
/// <param name="value">Value of the int</param>
|
||||
/// <param name="argumentName">Name of the parameter</param>
|
||||
/// <param name="allowedValues">Allowed values</param>
|
||||
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)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an int is between two values
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the int</param>
|
||||
/// <param name="argumentName">Name of the parameter</param>
|
||||
/// <param name="minValue">Min value</param>
|
||||
/// <param name="maxValue">Max value</param>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a string is not null or empty
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the string</param>
|
||||
/// <param name="argumentName">Name of the parameter</param>
|
||||
public static void ValidateNotNull(this string value, string argumentName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
throw new ArgumentException($"No value provided for parameter {argumentName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an object is not null
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the object</param>
|
||||
/// <param name="argumentName">Name of the parameter</param>
|
||||
public static void ValidateNotNull(this object value, string argumentName)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentException($"No value provided for parameter {argumentName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a list is not null or empty
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the object</param>
|
||||
/// <param name="argumentName">Name of the parameter</param>
|
||||
public static void ValidateNotNull<T>(this IEnumerable<T> value, string argumentName)
|
||||
{
|
||||
if (value == null || !value.Any())
|
||||
throw new ArgumentException($"No values provided for parameter {argumentName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The uri of the request
|
||||
/// Accept header
|
||||
/// </summary>
|
||||
string Accept { set; }
|
||||
/// <summary>
|
||||
/// Content
|
||||
/// </summary>
|
||||
string? Content { get; }
|
||||
/// <summary>
|
||||
/// Method
|
||||
/// </summary>
|
||||
HttpMethod Method { get; set; }
|
||||
/// <summary>
|
||||
/// Uri
|
||||
/// </summary>
|
||||
Uri Uri { get; }
|
||||
/// <summary>
|
||||
/// The headers of the request
|
||||
/// Set byte content
|
||||
/// </summary>
|
||||
WebHeaderCollection Headers { get; set; }
|
||||
/// <param name="data"></param>
|
||||
void SetContent(byte[] data);
|
||||
/// <summary>
|
||||
/// The method of the request
|
||||
/// Set string content
|
||||
/// </summary>
|
||||
string Method { get; set; }
|
||||
/// <summary>
|
||||
/// The timeout of the request
|
||||
/// </summary>
|
||||
TimeSpan Timeout { get; set; }
|
||||
/// <summary>
|
||||
/// Set a proxy
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="login"></param>
|
||||
/// <param name="password"></param>
|
||||
void SetProxy(string host, int port, string login, string password);
|
||||
/// <param name="data"></param>
|
||||
/// <param name="contentType"></param>
|
||||
void SetContent(string data, string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Content type
|
||||
/// Add a header to the request
|
||||
/// </summary>
|
||||
string ContentType { get; set; }
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
void AddHeader(string key, string value);
|
||||
/// <summary>
|
||||
/// String content
|
||||
/// </summary>
|
||||
string Content { get; set; }
|
||||
/// <summary>
|
||||
/// Accept
|
||||
/// </summary>
|
||||
string Accept { get; set; }
|
||||
/// <summary>
|
||||
/// Content length
|
||||
/// </summary>
|
||||
long ContentLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the request stream
|
||||
/// Get the response
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<Stream> GetRequestStream();
|
||||
/// <summary>
|
||||
/// Get the response object
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<IResponse> GetResponse();
|
||||
Task<IResponse> GetResponse(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Request factory interface
|
||||
@ -8,8 +12,16 @@
|
||||
/// <summary>
|
||||
/// Create a request for an uri
|
||||
/// </summary>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="uri"></param>
|
||||
/// <returns></returns>
|
||||
IRequest Create(string uri);
|
||||
IRequest Create(HttpMethod method, string uri);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the requests created by this factory
|
||||
/// </summary>
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
/// <param name="proxy">Proxy settings to use</param>
|
||||
void Configure(TimeSpan requestTimeout, ApiProxy? proxy);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
HttpStatusCode StatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the status code indicates a success status
|
||||
/// </summary>
|
||||
bool IsSuccessStatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the response stream
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Stream GetResponseStream();
|
||||
/// <summary>
|
||||
/// Get the response headers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<Tuple<string, string>> GetResponseHeaders();
|
||||
Task<Stream> GetResponseStream();
|
||||
|
||||
/// <summary>
|
||||
/// Close the response
|
||||
/// </summary>
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
CallResult<long> Ping();
|
||||
CallResult<long> Ping(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
Task<CallResult<long>> PingAsync();
|
||||
Task<CallResult<long>> PingAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
@ -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
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
|
||||
TimeSpan ResponseTimeout { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
|
||||
TimeSpan SocketNoDataTimeout { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of concurrent socket connections
|
||||
/// </summary>
|
||||
int MaxSocketConnections { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
|
||||
int SocketCombineTarget { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from a stream
|
||||
/// </summary>
|
||||
|
97
CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs
Normal file
97
CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for order book
|
||||
/// </summary>
|
||||
public interface ISymbolOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// The status of the order book. Order book is up to date when the status is `Synced`
|
||||
/// </summary>
|
||||
OrderBookStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last update identifier
|
||||
/// </summary>
|
||||
long LastSequenceNumber { get; }
|
||||
/// <summary>
|
||||
/// The symbol of the order book
|
||||
/// </summary>
|
||||
string Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event when the state changes
|
||||
/// </summary>
|
||||
event Action<OrderBookStatus, OrderBookStatus> OnStatusChange;
|
||||
/// <summary>
|
||||
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
|
||||
/// </summary>
|
||||
event Action<IEnumerable<ISymbolOrderBookEntry>, IEnumerable<ISymbolOrderBookEntry>> OnOrderBookUpdate;
|
||||
/// <summary>
|
||||
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
|
||||
/// </summary>
|
||||
event Action<ISymbolOrderBookEntry, ISymbolOrderBookEntry> OnBestOffersChanged;
|
||||
/// <summary>
|
||||
/// Timestamp of the last update
|
||||
/// </summary>
|
||||
DateTime LastOrderBookUpdate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of asks in the book
|
||||
/// </summary>
|
||||
int AskCount { get; }
|
||||
/// <summary>
|
||||
/// The number of bids in the book
|
||||
/// </summary>
|
||||
int BidCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of asks
|
||||
/// </summary>
|
||||
IEnumerable<ISymbolOrderBookEntry> Asks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of bids
|
||||
/// </summary>
|
||||
IEnumerable<ISymbolOrderBookEntry> Bids { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The best bid currently in the order book
|
||||
/// </summary>
|
||||
ISymbolOrderBookEntry BestBid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The best ask currently in the order book
|
||||
/// </summary>
|
||||
ISymbolOrderBookEntry BestAsk { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start connecting and synchronizing the order book
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
CallResult<bool> Start();
|
||||
|
||||
/// <summary>
|
||||
/// Start connecting and synchronizing the order book
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<bool>> StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Stop syncing the order book
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Stop syncing the order book
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace CryptoExchange.Net.OrderBook
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for order book entries
|
||||
@ -14,4 +14,15 @@
|
||||
/// </summary>
|
||||
decimal Price { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for order book entries
|
||||
/// </summary>
|
||||
public interface ISymbolOrderSequencedBookEntry: ISymbolOrderBookEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Sequence of the update
|
||||
/// </summary>
|
||||
long Sequence { get; set; }
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Origin
|
||||
/// </summary>
|
||||
string Origin { get; set; }
|
||||
string? Origin { get; set; }
|
||||
/// <summary>
|
||||
/// Reconnecting
|
||||
/// </summary>
|
||||
@ -42,11 +42,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Handler for byte data
|
||||
/// </summary>
|
||||
Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||
/// <summary>
|
||||
/// Handler for string data
|
||||
/// </summary>
|
||||
Func<string, string> DataInterpreterString { get; set; }
|
||||
Func<string, string>? DataInterpreterString { get; set; }
|
||||
/// <summary>
|
||||
/// Socket url
|
||||
/// </summary>
|
||||
@ -64,14 +64,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
bool IsOpen { get; }
|
||||
/// <summary>
|
||||
/// Should ping connecting
|
||||
/// </summary>
|
||||
bool PingConnection { get; set; }
|
||||
/// <summary>
|
||||
/// Interval of pinging
|
||||
/// </summary>
|
||||
TimeSpan PingInterval { get; set; }
|
||||
/// <summary>
|
||||
/// Supported ssl protocols
|
||||
/// </summary>
|
||||
SslProtocols SSLProtocols { get; set; }
|
||||
|
@ -12,7 +12,7 @@ namespace CryptoExchange.Net.Logging
|
||||
private static readonly object openedFilesLock = new object();
|
||||
private static readonly List<string> openedFiles = new List<string>();
|
||||
|
||||
private StreamWriter logWriter;
|
||||
private readonly StreamWriter logWriter;
|
||||
private readonly object writeLock;
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -50,10 +50,7 @@ namespace CryptoExchange.Net.Logging
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
lock (writeLock)
|
||||
{
|
||||
logWriter.Close();
|
||||
logWriter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@ -19,25 +20,20 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The login of the proxy
|
||||
/// </summary>
|
||||
public string Login { get; }
|
||||
public string? Login { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The password of the proxy
|
||||
/// </summary>
|
||||
public string Password { get; }
|
||||
public SecureString? Password { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
/// </summary>
|
||||
/// <param name="host">The proxy hostname/ip</param>
|
||||
/// <param name="port">The proxy port</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -48,11 +44,25 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="port">The proxy port</param>
|
||||
/// <param name="login">The proxy login</param>
|
||||
/// <param name="password">The proxy password</param>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
/// </summary>
|
||||
/// <param name="host">The proxy hostname/ip</param>
|
||||
/// <param name="port">The proxy port</param>
|
||||
/// <param name="login">The proxy login</param>
|
||||
/// <param name="password">The proxy password</param>
|
||||
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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// An error if the call didn't succeed
|
||||
/// </summary>
|
||||
public Error Error { get; internal set; }
|
||||
public Error? Error { get; internal set; }
|
||||
/// <summary>
|
||||
/// Whether the call was successful
|
||||
/// </summary>
|
||||
@ -28,11 +28,20 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public CallResult(T data, Error error)
|
||||
public CallResult([AllowNull]T data, Error? error)
|
||||
{
|
||||
Data = data;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public static implicit operator bool(CallResult<T> obj)
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -49,7 +58,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public IEnumerable<Tuple<string, string>> ResponseHeaders { get; set; }
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -58,7 +67,9 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(HttpStatusCode? code, IEnumerable<Tuple<string, string>> responseHeaders, T data, Error error): base(data, error)
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error)
|
||||
{
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseStatusCode = code;
|
||||
@ -71,7 +82,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public static WebCallResult<T> CreateErrorResult(Error error)
|
||||
{
|
||||
return new WebCallResult<T>(null, null, default(T), error);
|
||||
return new WebCallResult<T>(null, null, default!, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -81,9 +92,9 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult<T> CreateErrorResult(HttpStatusCode? code, IEnumerable<Tuple<string, string>> responseHeaders, Error error)
|
||||
public static WebCallResult<T> CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
|
||||
{
|
||||
return new WebCallResult<T>(code, responseHeaders, default(T), error);
|
||||
return new WebCallResult<T>(code, responseHeaders, default!, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,23 +5,6 @@
|
||||
/// </summary>
|
||||
public class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// GET Http method
|
||||
/// </summary>
|
||||
public const string GetMethod = "GET";
|
||||
/// <summary>
|
||||
/// POST Http method
|
||||
/// </summary>
|
||||
public const string PostMethod = "POST";
|
||||
/// <summary>
|
||||
/// DELETE Http method
|
||||
/// </summary>
|
||||
public const string DeleteMethod = "DELETE";
|
||||
/// <summary>
|
||||
/// PUT Http method
|
||||
/// </summary>
|
||||
public const string PutMethod = "PUT";
|
||||
|
||||
/// <summary>
|
||||
/// Json content type header
|
||||
/// </summary>
|
||||
|
@ -65,7 +65,7 @@
|
||||
/// <summary>
|
||||
/// Data synced, order book is up to date
|
||||
/// </summary>
|
||||
Synced,
|
||||
Synced
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -82,4 +82,19 @@
|
||||
/// </summary>
|
||||
Bid
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define how array parameters should be send
|
||||
/// </summary>
|
||||
public enum ArrayParametersSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Send multiple key=value for each entry
|
||||
/// </summary>
|
||||
MultipleValues,
|
||||
/// <summary>
|
||||
/// Create an []=value array
|
||||
/// </summary>
|
||||
Array
|
||||
}
|
||||
}
|
||||
|
@ -14,15 +14,22 @@
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional data for the error
|
||||
/// </summary>
|
||||
public object? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
protected Error(int code, string message)
|
||||
/// <param name="data"></param>
|
||||
protected Error(int code, string message, object? data)
|
||||
{
|
||||
Code = code;
|
||||
Message = message;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -31,7 +38,7 @@
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Code}: {Message}";
|
||||
return $"{Code}: {Message} {Data}";
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,7 +50,7 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CantConnectError() : base(1, "Can't connect to the server") { }
|
||||
public CantConnectError() : base(1, "Can't connect to the server", null) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -54,7 +61,7 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint") { }
|
||||
public NoApiCredentialsError() : base(2, "No credentials provided for private endpoint", null) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -66,14 +73,16 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ServerError(string message) : base(3, "Server error: " + message) { }
|
||||
/// <param name="data"></param>
|
||||
public ServerError(string message, object? data = null) : base(3, "Server error: " + message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
public ServerError(int code, string message) : base(code, message)
|
||||
/// <param name="data"></param>
|
||||
public ServerError(int code, string message, object? data = null) : base(code, message, data)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -86,8 +95,8 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public WebError(string message) : base(4, "Web error: " + message) { }
|
||||
/// <param name="data"></param>
|
||||
public WebError(object? data) : base(4, "Web error", data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -98,8 +107,8 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public DeserializeError(string message) : base(5, "Error deserializing data: " + message) { }
|
||||
/// <param name="data">Deserializing data</param>
|
||||
public DeserializeError(object? data) : base(5, "Error deserializing data", data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -110,8 +119,8 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public UnknownError(string message) : base(6, "Unknown error occured " + message) { }
|
||||
/// <param name="data">Error data</param>
|
||||
public UnknownError(object? data = null) : base(6, "Unknown error occured", data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -123,7 +132,7 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ArgumentError(string message) : base(7, "Invalid parameter: " + message) { }
|
||||
public ArgumentError(string message) : base(7, "Invalid parameter: " + message, null) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -135,6 +144,17 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message) { }
|
||||
public RateLimitError(string message) : base(8, "Rate limit exceeded: " + message, null) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation requested
|
||||
/// </summary>
|
||||
public class CancellationRequestedError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CancellationRequestedError() : base(9, "Cancellation requested", null) { }
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,12 @@ namespace CryptoExchange.Net.Objects
|
||||
/// The log writers
|
||||
/// </summary>
|
||||
public List<TextWriter> LogWriters { get; set; } = new List<TextWriter> { new DebugTextWriter() };
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"LogVerbosity: {LogVerbosity}, Writers: {LogWriters.Count}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -47,6 +53,12 @@ namespace CryptoExchange.Net.Objects
|
||||
OrderBookName = name;
|
||||
SequenceNumbersAreConsecutive = sequencesAreConsecutive;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -54,21 +66,36 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class ClientOptions : BaseOptions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials
|
||||
/// </summary>
|
||||
public ApiCredentials ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Proxy to use
|
||||
/// </summary>
|
||||
public ApiProxy Proxy { get; set; }
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress"></param>
|
||||
public ClientOptions(string baseAddress)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-": "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null? "-": Proxy.Host)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -91,6 +118,14 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress"></param>
|
||||
public RestClientOptions(string baseAddress): base(baseAddress)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of the options
|
||||
/// </summary>
|
||||
@ -110,10 +145,16 @@ namespace CryptoExchange.Net.Objects
|
||||
};
|
||||
|
||||
if (ApiCredentials != null)
|
||||
copy.ApiCredentials = new ApiCredentials(ApiCredentials.Key.GetString(), ApiCredentials.Secret.GetString());
|
||||
copy.ApiCredentials = ApiCredentials.Copy();
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -146,6 +187,14 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress"></param>
|
||||
public SocketClientOptions(string baseAddress) : base(baseAddress)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of the options
|
||||
/// </summary>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book entry
|
||||
/// </summary>
|
||||
public class OrderBookEntry : ISymbolOrderBookEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Quantity of the entry
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Price of the entry
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="price"></param>
|
||||
/// <param name="quantity"></param>
|
||||
public OrderBookEntry(decimal price, decimal quantity)
|
||||
{
|
||||
Quantity = quantity;
|
||||
Price = price;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// The first sequence number of the entries
|
||||
/// List of asks
|
||||
/// </summary>
|
||||
public long FirstSequence { get; set; }
|
||||
public IEnumerable<ISymbolOrderSequencedBookEntry> Asks { get; set; } = new List<ISymbolOrderSequencedBookEntry>();
|
||||
/// <summary>
|
||||
/// The last sequence number of the entries
|
||||
/// List of bids
|
||||
/// </summary>
|
||||
public long LastSequence { get; set; }
|
||||
/// <summary>
|
||||
/// List of entries
|
||||
/// </summary>
|
||||
public List<ProcessEntry> Entries { get; set; }
|
||||
public IEnumerable<ISymbolOrderSequencedBookEntry> Bids { get; set; } = new List<ISymbolOrderSequencedBookEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Buffer entry with a single update id per update
|
||||
/// </summary>
|
||||
public ProcessBufferEntry()
|
||||
public class ProcessBufferSingleSequenceEntry
|
||||
{
|
||||
Entries = new List<ProcessEntry>();
|
||||
}
|
||||
/// <summary>
|
||||
/// First update id
|
||||
/// </summary>
|
||||
public long UpdateId { get; set; }
|
||||
/// <summary>
|
||||
/// List of asks
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
|
||||
/// <summary>
|
||||
/// List of bids
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Buffer entry with a first and last update id
|
||||
/// </summary>
|
||||
public class ProcessBufferRangeSequenceEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// First update id
|
||||
/// </summary>
|
||||
public long FirstUpdateId { get; set; }
|
||||
/// <summary>
|
||||
/// Last update id
|
||||
/// </summary>
|
||||
public long LastUpdateId { get; set; }
|
||||
/// <summary>
|
||||
/// List of asks
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
|
||||
/// <summary>
|
||||
/// List of bids
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Process entry for order book
|
||||
/// </summary>
|
||||
public class ProcessEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry
|
||||
/// </summary>
|
||||
public ISymbolOrderBookEntry Entry { get; set; }
|
||||
/// <summary>
|
||||
/// The type
|
||||
/// </summary>
|
||||
public OrderBookEntryType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="entry"></param>
|
||||
public ProcessEntry(OrderBookEntryType type, ISymbolOrderBookEntry entry)
|
||||
{
|
||||
Type = type;
|
||||
Entry = entry;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,31 +14,39 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <summary>
|
||||
/// Base for order book implementations
|
||||
/// </summary>
|
||||
public abstract class SymbolOrderBook: IDisposable
|
||||
public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The process buffer, used while syncing
|
||||
/// </summary>
|
||||
protected readonly List<ProcessBufferEntry> processBuffer;
|
||||
protected readonly List<object> processBuffer;
|
||||
private readonly object bookLock = new object();
|
||||
/// <summary>
|
||||
/// The ask list
|
||||
/// </summary>
|
||||
protected SortedList<decimal, OrderBookEntry> asks;
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
|
||||
/// <summary>
|
||||
/// The bid list
|
||||
/// </summary>
|
||||
protected SortedList<decimal, OrderBookEntry> bids;
|
||||
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
|
||||
private OrderBookStatus status;
|
||||
private UpdateSubscription subscription;
|
||||
private UpdateSubscription? subscription;
|
||||
private readonly bool sequencesAreConsecutive;
|
||||
private readonly string id;
|
||||
|
||||
/// <summary>
|
||||
/// Order book implementation id
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
/// <summary>
|
||||
/// The log
|
||||
/// </summary>
|
||||
protected Log log;
|
||||
|
||||
private bool bookSet;
|
||||
/// <summary>
|
||||
/// If order book is set
|
||||
/// </summary>
|
||||
protected bool bookSet;
|
||||
|
||||
/// <summary>
|
||||
/// The status of the order book. Order book is up to date when the status is `Synced`
|
||||
@ -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
|
||||
/// <summary>
|
||||
/// Event when the state changes
|
||||
/// </summary>
|
||||
public event Action<OrderBookStatus, OrderBookStatus> OnStatusChange;
|
||||
public event Action<OrderBookStatus, OrderBookStatus>? OnStatusChange;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
|
||||
/// </summary>
|
||||
public event Action<ISymbolOrderBookEntry, ISymbolOrderBookEntry>? OnBestOffersChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public event Action<IEnumerable<ISymbolOrderBookEntry>, IEnumerable<ISymbolOrderBookEntry>>? OnOrderBookUpdate;
|
||||
/// <summary>
|
||||
/// Timestamp of the last update
|
||||
/// </summary>
|
||||
public DateTime LastOrderBookUpdate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of asks in the book
|
||||
@ -135,14 +158,20 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <param name="options"></param>
|
||||
protected SymbolOrderBook(string symbol, OrderBookOptions options)
|
||||
{
|
||||
id = options.OrderBookName;
|
||||
processBuffer = new List<ProcessBufferEntry>();
|
||||
if (symbol == null)
|
||||
throw new ArgumentNullException(nameof(symbol));
|
||||
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
Id = options.OrderBookName;
|
||||
processBuffer = new List<object>();
|
||||
sequencesAreConsecutive = options.SequenceNumbersAreConsecutive;
|
||||
Symbol = symbol;
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
|
||||
asks = new SortedList<decimal, OrderBookEntry>();
|
||||
bids = new SortedList<decimal, OrderBookEntry>(new DescComparer<decimal>());
|
||||
asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
||||
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
||||
|
||||
log = new Log { Level = options.LogVerbosity };
|
||||
var writers = options.LogWriters ?? new List<TextWriter> { 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<bool>(false, startResult.Error);
|
||||
|
||||
subscription = startResult.Data;
|
||||
subscription.ConnectionLost += Reset;
|
||||
subscription.ConnectionRestored += (time) => Resync();
|
||||
subscription.ConnectionRestored += time => Resync();
|
||||
Status = OrderBookStatus.Synced;
|
||||
return new CallResult<bool>(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,6 +241,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
public async Task StopAsync()
|
||||
{
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
if(subscription != null)
|
||||
await subscription.Close().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -238,127 +268,290 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <param name="orderBookSequenceNumber">The last update sequence number</param>
|
||||
/// <param name="askList">List of asks</param>
|
||||
/// <param name="bidList">List of bids</param>
|
||||
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> askList, IEnumerable<ISymbolOrderBookEntry> bidList)
|
||||
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> 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));
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the order book with entries
|
||||
/// Update the order book using a single id for an update
|
||||
/// </summary>
|
||||
/// <param name="firstSequenceNumber">First sequence number</param>
|
||||
/// <param name="lastSequenceNumber">Last sequence number</param>
|
||||
/// <param name="entries">List of entries</param>
|
||||
protected void UpdateOrderBook(long firstSequenceNumber, long lastSequenceNumber, List<ProcessEntry> entries)
|
||||
/// <param name="rangeUpdateId"></param>
|
||||
/// <param name="bids"></param>
|
||||
/// <param name="asks"></param>
|
||||
protected void UpdateOrderBook(long rangeUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the order book using a first/last update id
|
||||
/// </summary>
|
||||
/// <param name="firstUpdateId"></param>
|
||||
/// <param name="lastUpdateId"></param>
|
||||
/// <param name="bids"></param>
|
||||
/// <param name="asks"></param>
|
||||
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the order book using sequenced entries
|
||||
/// </summary>
|
||||
/// <param name="bids">List of bids</param>
|
||||
/// <param name="asks">List of asks</param>
|
||||
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> 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<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks)
|
||||
{
|
||||
var entries = new Dictionary<ISymbolOrderSequencedBookEntry, OrderBookEntryType>();
|
||||
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<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> 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<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> 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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check and empty the process buffer; see what entries to update the book with
|
||||
/// </summary>
|
||||
protected void CheckProcessBuffer()
|
||||
{
|
||||
foreach (var bufferEntry in processBuffer.OrderBy(b => b.FirstSequence).ToList())
|
||||
{
|
||||
if(bufferEntry.LastSequence < LastSequenceNumber)
|
||||
{
|
||||
processBuffer.Remove(bufferEntry);
|
||||
continue;
|
||||
}
|
||||
var pbList = processBuffer.ToList();
|
||||
if(pbList.Count > 0)
|
||||
log.Write(LogVerbosity.Debug, "Processing buffered updates");
|
||||
|
||||
if (bufferEntry.FirstSequence > LastSequenceNumber + 1)
|
||||
break;
|
||||
foreach (var bufferEntry in pbList)
|
||||
{
|
||||
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);
|
||||
|
||||
foreach(var entry in bufferEntry.Entries)
|
||||
ProcessUpdate(entry.Type, entry.Entry);
|
||||
processBuffer.Remove(bufferEntry);
|
||||
LastSequenceNumber = bufferEntry.LastSequence;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update order book with an entry
|
||||
/// </summary>
|
||||
/// <param name="sequence">Sequence number of the update</param>
|
||||
/// <param name="type">Type of entry</param>
|
||||
/// <param name="entry">The entry</param>
|
||||
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<decimal, OrderBookEntry>)))
|
||||
{
|
||||
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<decimal, OrderBookEntry>)))
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait until the order book has been set
|
||||
/// </summary>
|
||||
/// <param name="timeout">Max wait time</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<bool>> WaitForSetOrderBook(int timeout)
|
||||
{
|
||||
var startWait = DateTime.UtcNow;
|
||||
while (!bookSet && Status == OrderBookStatus.Syncing)
|
||||
{
|
||||
if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout)
|
||||
return new CallResult<bool>(false, new ServerError("Timeout while waiting for data"));
|
||||
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new CallResult<bool>(true, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the order book
|
||||
/// </summary>
|
||||
|
@ -32,10 +32,10 @@ namespace CryptoExchange.Net.RateLimiter
|
||||
/// <inheritdoc />
|
||||
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour)
|
||||
{
|
||||
if(client.authProvider?.Credentials == null)
|
||||
if(client.authProvider?.Credentials?.Key == null)
|
||||
return new CallResult<double>(0, null);
|
||||
|
||||
string key = client.authProvider.Credentials.Key.GetString();
|
||||
var key = client.authProvider.Credentials.Key.GetString();
|
||||
|
||||
int waitTime;
|
||||
RateLimitObject rlo;
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public class Request : IRequest
|
||||
{
|
||||
private readonly WebRequest request;
|
||||
private readonly HttpRequestMessage request;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Create request object for web request
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
public Request(WebRequest request)
|
||||
/// <param name="client"></param>
|
||||
public Request(HttpRequestMessage request, HttpClient client)
|
||||
{
|
||||
httpClient = client;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public WebHeaderCollection Headers
|
||||
{
|
||||
get => request.Headers;
|
||||
set => request.Headers = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ContentType
|
||||
{
|
||||
get => request.ContentType;
|
||||
set => request.ContentType = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Content { get; set; }
|
||||
public string? Content { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Accept
|
||||
{
|
||||
get => ((HttpWebRequest)request).Accept;
|
||||
set => ((HttpWebRequest)request).Accept = value;
|
||||
set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long ContentLength
|
||||
{
|
||||
get => ((HttpWebRequest)request).ContentLength;
|
||||
set => ((HttpWebRequest)request).ContentLength = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Method
|
||||
public HttpMethod Method
|
||||
{
|
||||
get => request.Method;
|
||||
set => request.Method = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan Timeout
|
||||
{
|
||||
get => TimeSpan.FromMilliseconds(request.Timeout);
|
||||
set => request.Timeout = (int)Math.Round(value.TotalMilliseconds);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => request.RequestUri;
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Stream> GetRequestStream()
|
||||
public void AddHeader(string key, string value)
|
||||
{
|
||||
return await request.GetRequestStreamAsync().ConfigureAwait(false);
|
||||
request.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IResponse> GetResponse()
|
||||
public void SetContent(byte[] data)
|
||||
{
|
||||
return new Response((HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false));
|
||||
request.Content = new ByteArrayContent(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IResponse> GetResponse(CancellationToken cancellationToken)
|
||||
{
|
||||
return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
private HttpClient? httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
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};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// HttpWebResponse response object
|
||||
/// </summary>
|
||||
public class Response : IResponse
|
||||
internal class Response : IResponse
|
||||
{
|
||||
private readonly HttpWebResponse response;
|
||||
private readonly HttpResponseMessage response;
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpStatusCode StatusCode => response.StatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => response.Headers;
|
||||
|
||||
/// <summary>
|
||||
/// Create response for http web response
|
||||
/// Create response for a http response message
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
public Response(HttpWebResponse response)
|
||||
/// <param name="response">The actual response</param>
|
||||
public Response(HttpResponseMessage response)
|
||||
{
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Stream GetResponseStream()
|
||||
public async Task<Stream> GetResponseStream()
|
||||
{
|
||||
return response.GetResponseStream();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Tuple<string, string>> GetResponseHeaders()
|
||||
{
|
||||
return response.Headers.ToIEnumerable();
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Close()
|
||||
{
|
||||
response.Close();
|
||||
response.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Where to place post parameters
|
||||
/// </summary>
|
||||
@ -39,16 +39,21 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters
|
||||
/// </summary>
|
||||
protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for requests
|
||||
/// </summary>
|
||||
protected TimeSpan RequestTimeout { get; private set; }
|
||||
public TimeSpan RequestTimeout { get; }
|
||||
/// <summary>
|
||||
/// Rate limiting behaviour
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitBehaviour { get; private set; }
|
||||
public RateLimitingBehaviour RateLimitBehaviour { get; }
|
||||
/// <summary>
|
||||
/// List of ratelimitters
|
||||
/// List of rate limiters
|
||||
/// </summary>
|
||||
public IEnumerable<IRateLimiter> RateLimiters { get; private set; }
|
||||
/// <summary>
|
||||
@ -61,18 +66,13 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions"></param>
|
||||
/// <param name="authenticationProvider"></param>
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// Configure the client using the provided options
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions">Options</param>
|
||||
protected void Configure(RestClientOptions exchangeOptions)
|
||||
{
|
||||
RequestTimeout = exchangeOptions.RequestTimeout;
|
||||
RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy);
|
||||
RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in exchangeOptions.RateLimiters)
|
||||
@ -86,6 +86,9 @@ namespace CryptoExchange.Net
|
||||
/// <param name="limiter">The limiter to add</param>
|
||||
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
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
public virtual CallResult<long> Ping() => PingAsync().Result;
|
||||
public virtual CallResult<long> Ping(CancellationToken ct = default) => PingAsync(ct).Result;
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
public virtual async Task<CallResult<long>> PingAsync()
|
||||
public virtual async Task<CallResult<long>> 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<long>(0, new CantConnectError() {Message = "Ping failed: " + e.Message});
|
||||
return new CallResult<long>(0, new CantConnectError {Message = "Ping failed: " + e.Message});
|
||||
|
||||
if (e.InnerException is SocketException exception)
|
||||
return new CallResult<long>(0, new CantConnectError() { Message = "Ping failed: " + exception.SocketErrorCode });
|
||||
return new CallResult<long>(0, new CantConnectError() { Message = "Ping failed: " + e.InnerException.Message });
|
||||
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode });
|
||||
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message });
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctRegistration.Dispose();
|
||||
ping.Dispose();
|
||||
}
|
||||
|
||||
return reply.Status == IPStatus.Success ? new CallResult<long>(reply.RoundtripTime, null) : new CallResult<long>(0, new CantConnectError() { Message = "Ping failed: " + reply.Status });
|
||||
if(ct.IsCancellationRequested)
|
||||
return new CallResult<long>(0, new CancellationRequestedError());
|
||||
|
||||
return reply.Status == IPStatus.Success ? new CallResult<long>(reply.RoundtripTime, null) : new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + reply.Status });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -137,11 +150,14 @@ namespace CryptoExchange.Net
|
||||
/// <typeparam name="T">The expected result type</typeparam>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="checkResult">Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> ExecuteRequest<T>(Uri uri, string method = Constants.GetMethod, Dictionary<string, object> parameters = null, bool signed = false, bool checkResult = true) where T : class
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult<T>> SendRequest<T>(Uri uri, HttpMethod method, CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? 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)
|
||||
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<T>(result.ResponseStatusCode, result.ResponseHeaders, null, result.Error);
|
||||
|
||||
var jsonResult = ValidateJson(result.Data);
|
||||
if(!jsonResult.Success)
|
||||
return new WebCallResult<T>(result.ResponseStatusCode, result.ResponseHeaders, null, jsonResult.Error);
|
||||
|
||||
if (IsErrorResponse(jsonResult.Data))
|
||||
return new WebCallResult<T>(result.ResponseStatusCode, result.ResponseHeaders, null, ParseErrorResponse(jsonResult.Data));
|
||||
|
||||
var desResult = Deserialize<T>(jsonResult.Data, checkResult);
|
||||
if (!desResult.Success)
|
||||
return new WebCallResult<T>(result.ResponseStatusCode, result.ResponseHeaders, null, desResult.Error);
|
||||
|
||||
return new WebCallResult<T>(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<T>(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be overridden to indicate if a response is an error response
|
||||
/// Executes the request and returns the string result
|
||||
/// </summary>
|
||||
/// <param name="data">The received data</param>
|
||||
/// <returns>True if error response</returns>
|
||||
protected virtual bool IsErrorResponse(JToken data)
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
private async Task<WebCallResult<T>> GetResponse<T>(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<T>(responseStream).ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(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<T>(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<T>(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<T>(null, null, default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
log.Write(LogVerbosity.Warning, "Request timed out");
|
||||
return new WebCallResult<T>(null, null, default, new WebError("Request timed out"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -212,7 +251,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest ConstructRequest(Uri uri, string method, Dictionary<string, object> parameters, bool signed)
|
||||
protected virtual IRequest ConstructRequest(Uri uri, HttpMethod method, Dictionary<string, object>? parameters, bool signed)
|
||||
{
|
||||
if (parameters == null)
|
||||
parameters = new Dictionary<string, object>();
|
||||
@ -221,132 +260,51 @@ 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);
|
||||
if((method == HttpMethod.Get || method == HttpMethod.Delete || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true)
|
||||
uriString += "?" + parameters.CreateParamString(true, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(uriString);
|
||||
request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
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<string, string>();
|
||||
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);
|
||||
WriteParamBody(request, parameters, contentType);
|
||||
else
|
||||
WriteParamBody(request, "{}");
|
||||
request.SetContent("{}", contentType);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the string data of the parameters to the request body stream
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="stringData"></param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the parameters of the request to the request object, either in the query string or the request body
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="parameters"></param>
|
||||
protected virtual void WriteParamBody(IRequest request, Dictionary<string, object> parameters)
|
||||
/// <param name="contentType"></param>
|
||||
protected virtual void WriteParamBody(IRequest request, Dictionary<string, object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request and returns the string result
|
||||
/// </summary>
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <returns></returns>
|
||||
private async Task<WebCallResult<string>> 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<string>(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<string>(statusCode, returnHeaders, null, jsonResult.Error) : new WebCallResult<string>(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<string>(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<string>(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<string>(null, null, null, new UnknownError(e.Message + ", data: " + returnedData));
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.ReconnectInterval"/>
|
||||
public TimeSpan ReconnectInterval { get; private set; }
|
||||
public TimeSpan ReconnectInterval { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.AutoReconnect"/>
|
||||
public bool AutoReconnect { get; private set; }
|
||||
public bool AutoReconnect { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
|
||||
public TimeSpan ResponseTimeout { get; private set; }
|
||||
public TimeSpan ResponseTimeout { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
|
||||
public TimeSpan SocketNoDataTimeout { get; private set; }
|
||||
public TimeSpan SocketNoDataTimeout { get; }
|
||||
/// <summary>
|
||||
/// The max amount of concurrent socket connections
|
||||
/// </summary>
|
||||
@ -50,11 +51,11 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Handler for byte data
|
||||
/// </summary>
|
||||
protected Func<byte[], string> dataInterpreterBytes;
|
||||
protected Func<byte[], string>? dataInterpreterBytes;
|
||||
/// <summary>
|
||||
/// Handler for string data
|
||||
/// </summary>
|
||||
protected Func<string, string> dataInterpreterString;
|
||||
protected Func<string, string>? dataInterpreterString;
|
||||
/// <summary>
|
||||
/// Generic handlers
|
||||
/// </summary>
|
||||
@ -62,11 +63,11 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Periodic task
|
||||
/// </summary>
|
||||
protected Task periodicTask;
|
||||
protected Task? periodicTask;
|
||||
/// <summary>
|
||||
/// Periodic task event
|
||||
/// </summary>
|
||||
protected AutoResetEvent periodicEvent;
|
||||
protected AutoResetEvent? periodicEvent;
|
||||
/// <summary>
|
||||
/// Is disposing
|
||||
/// </summary>
|
||||
@ -84,17 +85,11 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions">Client options</param>
|
||||
/// <param name="authenticationProvider">Authentication provider</param>
|
||||
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));
|
||||
|
||||
/// <summary>
|
||||
/// Configure the client using the provided options
|
||||
/// </summary>
|
||||
/// <param name="exchangeOptions">Options</param>
|
||||
protected void Configure(SocketClientOptions exchangeOptions)
|
||||
{
|
||||
AutoReconnect = exchangeOptions.AutoReconnect;
|
||||
ReconnectInterval = exchangeOptions.ReconnectInterval;
|
||||
ResponseTimeout = exchangeOptions.SocketResponseTimeout;
|
||||
@ -107,7 +102,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="byteHandler">Handler for byte data</param>
|
||||
/// <param name="stringHandler">Handler for string data</param>
|
||||
protected void SetDataInterpreter(Func<byte[], string> byteHandler, Func<string, string> stringHandler)
|
||||
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
|
||||
{
|
||||
dataInterpreterBytes = byteHandler;
|
||||
dataInterpreterString = stringHandler;
|
||||
@ -122,7 +117,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="authenticated">If the subscription should be authenticated</param>
|
||||
/// <param name="dataHandler">The handler of update data</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<UpdateSubscription>> Subscribe<T>(object request, string identifier, bool authenticated, Action<T> dataHandler)
|
||||
protected virtual Task<CallResult<UpdateSubscription>> Subscribe<T>(object? request, string? identifier, bool authenticated, Action<T> dataHandler)
|
||||
{
|
||||
return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler);
|
||||
}
|
||||
@ -137,11 +132,11 @@ namespace CryptoExchange.Net
|
||||
/// <param name="authenticated">If the subscription should be authenticated</param>
|
||||
/// <param name="dataHandler">The handler of update data</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> Subscribe<T>(string url, object request, string identifier, bool authenticated, Action<T> dataHandler)
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> Subscribe<T>(string url, object? request, string? identifier, bool authenticated, Action<T> 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<UpdateSubscription>(null, connectResult.Error);
|
||||
}
|
||||
finally
|
||||
@ -166,19 +161,25 @@ 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<UpdateSubscription>(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<UpdateSubscription>(null, subResult.Error);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
handler.Confirmed = true;
|
||||
}
|
||||
|
||||
socket.ShouldReconnect = true;
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socket, handler), null);
|
||||
@ -193,14 +194,8 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription)
|
||||
{
|
||||
CallResult<object> callResult = null;
|
||||
await socket.SendAndWait(request, ResponseTimeout, (data) =>
|
||||
{
|
||||
if (!HandleSubscriptionResponse(socket, subscription, request, data, out callResult))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}).ConfigureAwait(false);
|
||||
CallResult<object>? 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
|
||||
/// <summary>
|
||||
/// Query for data
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Exepected result type</typeparam>
|
||||
/// <typeparam name="T">Expected result type</typeparam>
|
||||
/// <param name="request">The request to send</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
@ -231,7 +226,7 @@ namespace CryptoExchange.Net
|
||||
protected virtual async Task<CallResult<T>> Query<T>(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<T>(default(T), connectResult.Error);
|
||||
if (!connectResult)
|
||||
return new CallResult<T>(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<T>(default(T), new ServerError("Socket is paused"));
|
||||
return new CallResult<T>(default, new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
return await QueryAndWait<T>(socket, request).ConfigureAwait(false);
|
||||
@ -273,8 +268,8 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAndWait<T>(SocketConnection socket, object request)
|
||||
{
|
||||
CallResult<T> dataResult = new CallResult<T>(default(T), new ServerError("No response on query received"));
|
||||
await socket.SendAndWait(request, ResponseTimeout, (data) =>
|
||||
var dataResult = new CallResult<T>(default, new ServerError("No response on query received"));
|
||||
await socket.SendAndWait(request, ResponseTimeout, data =>
|
||||
{
|
||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||
return false;
|
||||
@ -294,28 +289,25 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<bool>> ConnectIfNeeded(SocketConnection socket, bool authenticated)
|
||||
{
|
||||
if (!socket.Connected)
|
||||
{
|
||||
var connectResult = await ConnectSocket(socket).ConfigureAwait(false);
|
||||
if (!connectResult.Success)
|
||||
{
|
||||
return new CallResult<bool>(false, new CantConnectError());
|
||||
}
|
||||
if (socket.Connected)
|
||||
return new CallResult<bool>(true, null);
|
||||
|
||||
var connectResult = await ConnectSocket(socket).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<bool>(false, new CantConnectError());
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true, null);
|
||||
|
||||
if (authenticated && !socket.Authenticated)
|
||||
{
|
||||
var result = await AuthenticateSocket(socket).ConfigureAwait(false);
|
||||
if (!result.Success)
|
||||
if (!result)
|
||||
{
|
||||
log.Write(LogVerbosity.Warning, "Socket authentication failed");
|
||||
result.Error.Message = "Authentication failed: " + result.Error.Message;
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult<bool>(false, result.Error);
|
||||
}
|
||||
|
||||
socket.Authenticated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<bool>(true, null);
|
||||
}
|
||||
|
||||
@ -328,7 +320,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="data">The message</param>
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the query</returns>
|
||||
protected internal abstract bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult);
|
||||
protected internal abstract bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, [NotNullWhen(true)]out CallResult<T>? callResult);
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
@ -338,7 +330,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="message">The message</param>
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the subscription request</returns>
|
||||
protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult<object> callResult);
|
||||
protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult<object>? callResult);
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler. Typically if an update message matches the request
|
||||
/// </summary>
|
||||
@ -387,9 +379,9 @@ namespace CryptoExchange.Net
|
||||
/// <param name="connection">The socket connection the handler is on</param>
|
||||
/// <param name="dataHandler">The handler of the data received</param>
|
||||
/// <returns></returns>
|
||||
protected virtual SocketSubscription AddHandler<T>(object request, string identifier, bool userSubscription, SocketConnection connection, Action<T> dataHandler)
|
||||
protected virtual SocketSubscription AddHandler<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<T> dataHandler)
|
||||
{
|
||||
Action<SocketConnection, JToken> internalHandler = (socketWrapper, data) =>
|
||||
void InternalHandler(SocketConnection socketWrapper, JToken data)
|
||||
{
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
@ -398,18 +390,20 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
var desResult = Deserialize<T>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -417,11 +411,12 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
|
||||
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(Newtonsoft.Json.Linq.JToken,string)"/>)</param>
|
||||
protected virtual void AddGenericHandler(string identifier, Action<SocketConnection, JToken> action)
|
||||
protected void AddGenericHandler(string identifier, Action<SocketConnection, JToken> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
public virtual void SendPeriodic(TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
{
|
||||
if (objGetter == null)
|
||||
throw new ArgumentNullException(nameof(objGetter));
|
||||
|
||||
periodicEvent = new AutoResetEvent(false);
|
||||
periodicTask = Task.Run(async () =>
|
||||
{
|
||||
@ -516,8 +518,9 @@ namespace CryptoExchange.Net
|
||||
break;
|
||||
|
||||
var obj = objGetter(socket);
|
||||
if (obj != null)
|
||||
{
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
socket.Send(obj);
|
||||
@ -528,8 +531,6 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
/// <returns></returns>
|
||||
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<Task>();
|
||||
{
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -21,52 +21,106 @@ namespace CryptoExchange.Net.Sockets
|
||||
internal static int lastStreamId;
|
||||
private static readonly object streamIdLock = new object();
|
||||
|
||||
protected WebSocket socket;
|
||||
/// <summary>
|
||||
/// Socket
|
||||
/// </summary>
|
||||
protected WebSocket? socket;
|
||||
/// <summary>
|
||||
/// Log
|
||||
/// </summary>
|
||||
protected Log log;
|
||||
protected object socketLock = new object();
|
||||
private readonly object socketLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Error handlers
|
||||
/// </summary>
|
||||
protected readonly List<Action<Exception>> errorHandlers = new List<Action<Exception>>();
|
||||
/// <summary>
|
||||
/// Open handlers
|
||||
/// </summary>
|
||||
protected readonly List<Action> openHandlers = new List<Action>();
|
||||
/// <summary>
|
||||
/// Close handlers
|
||||
/// </summary>
|
||||
protected readonly List<Action> closeHandlers = new List<Action>();
|
||||
/// <summary>
|
||||
/// Message handlers
|
||||
/// </summary>
|
||||
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
|
||||
|
||||
protected IDictionary<string, string> cookies;
|
||||
protected IDictionary<string, string> headers;
|
||||
protected HttpConnectProxy proxy;
|
||||
private readonly IDictionary<string, string> cookies;
|
||||
private readonly IDictionary<string, string> headers;
|
||||
private HttpConnectProxy? proxy;
|
||||
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
/// <summary>
|
||||
/// If is reconnecting
|
||||
/// </summary>
|
||||
public bool Reconnecting { get; set; }
|
||||
public string Origin { get; set; }
|
||||
/// <summary>
|
||||
/// Origin
|
||||
/// </summary>
|
||||
public string? Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Url
|
||||
/// </summary>
|
||||
public string Url { get; }
|
||||
public bool IsClosed => socket.State == WebSocketState.Closed;
|
||||
public bool IsOpen => socket.State == WebSocketState.Open;
|
||||
/// <summary>
|
||||
/// Is closed
|
||||
/// </summary>
|
||||
public bool IsClosed => socket?.State == null || socket.State == WebSocketState.Closed;
|
||||
/// <summary>
|
||||
/// Is open
|
||||
/// </summary>
|
||||
public bool IsOpen => socket?.State == WebSocketState.Open;
|
||||
/// <summary>
|
||||
/// Protocols
|
||||
/// </summary>
|
||||
public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
|
||||
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
public Func<string, string> DataInterpreterString { get; set; }
|
||||
/// <summary>
|
||||
/// Interpreter for bytes
|
||||
/// </summary>
|
||||
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||
/// <summary>
|
||||
/// Interpreter for strings
|
||||
/// </summary>
|
||||
public Func<string, string>? DataInterpreterString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last action time
|
||||
/// </summary>
|
||||
public DateTime LastActionTime { get; private set; }
|
||||
/// <summary>
|
||||
/// Timeout
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Socket state
|
||||
/// </summary>
|
||||
public WebSocketState SocketState => socket?.State ?? WebSocketState.None;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="url"></param>
|
||||
public BaseSocket(Log log, string url):this(log, url, new Dictionary<string, string>(), new Dictionary<string, string>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="cookies"></param>
|
||||
/// <param name="headers"></param>
|
||||
public BaseSocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
|
||||
{
|
||||
Id = NextStreamId();
|
||||
@ -106,27 +160,43 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On close
|
||||
/// </summary>
|
||||
public event Action OnClose
|
||||
{
|
||||
add => closeHandlers.Add(value);
|
||||
remove => closeHandlers.Remove(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// On message
|
||||
/// </summary>
|
||||
public event Action<string> OnMessage
|
||||
{
|
||||
add => messageHandlers.Add(value);
|
||||
remove => messageHandlers.Remove(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// On error
|
||||
/// </summary>
|
||||
public event Action<Exception> OnError
|
||||
{
|
||||
add => errorHandlers.Add(value);
|
||||
remove => errorHandlers.Remove(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// On open
|
||||
/// </summary>
|
||||
public event Action OnOpen
|
||||
{
|
||||
add => openHandlers.Add(value);
|
||||
remove => openHandlers.Remove(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle
|
||||
/// </summary>
|
||||
/// <param name="handlers"></param>
|
||||
protected void Handle(List<Action> handlers)
|
||||
{
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
@ -134,6 +204,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
handle?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="handlers"></param>
|
||||
/// <param name="data"></param>
|
||||
protected void Handle<T>(List<Action<T>> handlers, T data)
|
||||
{
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
@ -141,6 +217,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
handle?.Invoke(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if timed out
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset socket
|
||||
/// </summary>
|
||||
public virtual void Reset()
|
||||
{
|
||||
lock (socketLock)
|
||||
@ -206,11 +293,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
public virtual void Send(string data)
|
||||
{
|
||||
socket.Send(data);
|
||||
socket?.Send(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual Task<bool> 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,12 +362,14 @@ 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)
|
||||
socket.Close();
|
||||
@ -281,6 +378,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a proxy
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="port"></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (socketLock)
|
||||
|
@ -19,15 +19,23 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Connection lost event
|
||||
/// </summary>
|
||||
public event Action ConnectionLost;
|
||||
public event Action? ConnectionLost;
|
||||
/// <summary>
|
||||
/// Connecting restored event
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored;
|
||||
public event Action<TimeSpan>? ConnectionRestored;
|
||||
/// <summary>
|
||||
/// The connection is paused event
|
||||
/// </summary>
|
||||
public event Action? ActivityPaused;
|
||||
/// <summary>
|
||||
/// The connection is unpaused event
|
||||
/// </summary>
|
||||
public event Action? ActivityUnpaused;
|
||||
/// <summary>
|
||||
/// Connecting closed event
|
||||
/// </summary>
|
||||
public event Action Closed;
|
||||
public event Action? Closed;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of handlers
|
||||
@ -60,12 +68,27 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Time of disconnecting
|
||||
/// </summary>
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If activity is paused
|
||||
/// </summary>
|
||||
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<SocketSubscription> handlers;
|
||||
private bool pausedActivity;
|
||||
private readonly List<SocketSubscription> handlers;
|
||||
private readonly object handlersLock = new object();
|
||||
|
||||
private bool lostTriggered;
|
||||
@ -110,37 +133,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a handler
|
||||
/// </summary>
|
||||
/// <param name="request">The request object</param>
|
||||
/// <param name="userSubscription">If it is a user subscription or a generic handler</param>
|
||||
/// <param name="dataHandler">The data handler</param>
|
||||
/// <returns></returns>
|
||||
public SocketSubscription AddHandler(object request, bool userSubscription, Action<SocketConnection, JToken> dataHandler)
|
||||
{
|
||||
var handler = new SocketSubscription(null, request, userSubscription, dataHandler);
|
||||
lock (handlersLock)
|
||||
handlers.Add(handler);
|
||||
return handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a handler
|
||||
/// </summary>
|
||||
/// <param name="identifier">The identifier of the handler</param>
|
||||
/// <param name="userSubscription">If it is a user subscription or a generic handler</param>
|
||||
/// <param name="dataHandler">The data handler</param>
|
||||
/// <returns></returns>
|
||||
/// <returns></returns>
|
||||
public SocketSubscription AddHandler(string identifier, bool userSubscription, Action<SocketConnection, JToken> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add handler
|
||||
/// </summary>
|
||||
/// <param name="handler"></param>
|
||||
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,6 +239,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="nullValueHandling">How null values should be serialized</param>
|
||||
public virtual void Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
||||
{
|
||||
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<Task>();
|
||||
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<JToken, bool> Handler { get; }
|
||||
public JToken Result { get; private set; }
|
||||
public JToken? Result { get; private set; }
|
||||
public ManualResetEvent Event { get; }
|
||||
public TimeSpan Timeout { get; }
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Exception event
|
||||
/// </summary>
|
||||
public event Action<Exception> Exception;
|
||||
public event Action<Exception>? Exception;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <summary>
|
||||
/// Request object
|
||||
/// </summary>
|
||||
public object Request { get; set; }
|
||||
public object? Request { get; set; }
|
||||
/// <summary>
|
||||
/// Subscription identifier
|
||||
/// </summary>
|
||||
public string Identifier { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
/// <summary>
|
||||
/// Is user subscription or generic
|
||||
/// </summary>
|
||||
@ -36,20 +36,38 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public bool Confirmed { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="userSubscription"></param>
|
||||
/// <param name="dataHandler"></param>
|
||||
public SocketSubscription(string identifier, object request, bool userSubscription, Action<SocketConnection, JToken> dataHandler)
|
||||
private SocketSubscription(object? request, string? identifier, bool userSubscription, Action<SocketConnection, JToken> dataHandler)
|
||||
{
|
||||
UserSubscription = userSubscription;
|
||||
MessageHandler = dataHandler;
|
||||
Identifier = identifier;
|
||||
Request = request;
|
||||
Identifier = identifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create SocketSubscription for a request
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="userSubscription"></param>
|
||||
/// <param name="dataHandler"></param>
|
||||
/// <returns></returns>
|
||||
public static SocketSubscription CreateForRequest(object request, bool userSubscription,
|
||||
Action<SocketConnection, JToken> dataHandler)
|
||||
{
|
||||
return new SocketSubscription(request, null, userSubscription, dataHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create SocketSubscription for an identifier
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <param name="userSubscription"></param>
|
||||
/// <param name="dataHandler"></param>
|
||||
/// <returns></returns>
|
||||
public static SocketSubscription CreateForIdentifier(string identifier, bool userSubscription,
|
||||
Action<SocketConnection, JToken> dataHandler)
|
||||
{
|
||||
return new SocketSubscription(null, identifier, userSubscription, dataHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -29,6 +29,24 @@ namespace CryptoExchange.Net.Sockets
|
||||
remove => connection.ConnectionRestored -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection to the server is paused. No operations can be performed while paused
|
||||
/// </summary>
|
||||
public event Action ActivityPaused
|
||||
{
|
||||
add => connection.ActivityPaused += value;
|
||||
remove => connection.ActivityPaused -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection to the server is unpaused
|
||||
/// </summary>
|
||||
public event Action ActivityUnpaused
|
||||
{
|
||||
add => connection.ActivityUnpaused += value;
|
||||
remove => connection.ActivityUnpaused -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event when an exception happened
|
||||
/// </summary>
|
||||
|
60
README.md
60
README.md
@ -14,30 +14,34 @@ A base library for easy implementation of cryptocurrency API's. Include:
|
||||
## Implementations
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Resources/icon.png?raw=true"></a>
|
||||
<td><a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Bittrex.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/Bittrex.Net">Bittrex</a>
|
||||
</td>
|
||||
<td><a href="https://github.com/JKorf/Bitfinex.Net"><img src="https://github.com/JKorf/Bitfinex.Net/blob/master/Resources/icon.png?raw=true"></a>
|
||||
<td><a href="https://github.com/JKorf/Bitfinex.Net"><img src="https://github.com/JKorf/Bitfinex.Net/blob/master/Bitfinex.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/Bitfinex.Net">Bitfinex</a>
|
||||
</td>
|
||||
<td><a href="https://github.com/JKorf/Binance.Net"><img src="https://github.com/JKorf/Binance.Net/blob/master/Resources/binance-coin.png?raw=true"></a>
|
||||
<td><a href="https://github.com/JKorf/Binance.Net"><img src="https://github.com/JKorf/Binance.Net/blob/master/Binance.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/Binance.Net">Binance</a>
|
||||
</td>
|
||||
<td><a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/Resources/icon.png?raw=true"></a>
|
||||
<td><a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/CoinEx.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/CoinEx.Net">CoinEx</a>
|
||||
</td>
|
||||
<td><a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Resources/icon.png?raw=true"></a>
|
||||
<td><a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/Huobi.Net">Huobi</a>
|
||||
</td>
|
||||
<td><a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Resources/icon.png?raw=true"></a>
|
||||
<td><a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/Kucoin.Net">Kucoin</a>
|
||||
</td>
|
||||
<td><a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>
|
||||
<br />
|
||||
<a href="https://github.com/JKorf/Kraken.Net">Kraken</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user