mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d89c2bde94 | |||
| 3e6bdaafc6 | |||
| 93e4722a81 | |||
| 355ecb03da | |||
| 994c527c1d | |||
| 69b2e2045e | |||
| 7fde8bf5da | |||
| 637070a7ae | |||
| 7be75f72a7 | |||
| e3fece41f3 | |||
| 87b0c8d7a2 | |||
| ca9a711f22 | |||
| 27597bc994 | |||
| 776d75170d | |||
| 949780a9ad | |||
| 2f64cd9f05 | |||
| 185dfeb6fb | |||
| 68067d6258 | |||
| b309deb0c4 | |||
| e1dafdf0dd | |||
| fd7b5f0f0f | |||
| 3b735d66fd | |||
| 3cd505ac8b | |||
| 81d856d78d | |||
| 11c1ad871a | |||
| ffcb7db8ff | |||
| 02432e5109 | |||
| a85bfb4432 | |||
| 17d85fdd85 | |||
| 5e0733d7f4 | |||
| 28d5287bd4 | |||
| ef5097589a | |||
| f287ec1fa4 | |||
| 28da93af9d | |||
| 0d5bdf5095 | |||
| 8dac3d7aa6 | |||
| 6951f31be7 | |||
| 630f85ec49 | |||
| 9ec4f2276f | |||
| 0a0c66541e | |||
| bb4199620e | |||
| 8a83cd2cb8 | |||
| fcfeaf568f | |||
| 25567ea434 | |||
| 1ab85d4c26 | |||
| be68115099 | |||
| ff0550b0fb | |||
| 1ab1e008fc |
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
@@ -51,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
// assert
|
||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
||||
Assert.That(options.ApiCredentials.Key.GetString() == "123");
|
||||
Assert.That(options.ApiCredentials.Secret.GetString() == "456");
|
||||
Assert.That(options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.ApiCredentials.Secret == "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -64,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -176,12 +176,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(i == requests? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -243,12 +243,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -309,9 +309,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
@@ -348,9 +348,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -365,9 +365,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +58,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
Dictionary<string, string> result = null;
|
||||
@@ -75,7 +73,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
sub.AddSubscription(subObj);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
|
||||
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
@@ -93,9 +91,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.SubOptions.OutputOriginalData = enabled;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
@@ -107,7 +103,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rstEvent.Set();
|
||||
});
|
||||
sub.AddSubscription(subObj);
|
||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 });
|
||||
|
||||
// act
|
||||
socket.InvokeMessage(msgToSend);
|
||||
@@ -202,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
// act
|
||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" }));
|
||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" }));
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
@@ -225,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
// act
|
||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" }));
|
||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" }));
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
|
||||
@@ -163,6 +163,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", TestEnum.One)]
|
||||
[TestCase(null, TestEnum.One)]
|
||||
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
||||
{
|
||||
var result = EnumConverter.ParseString<TestEnum>(value);
|
||||
Assert.That(result == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
@@ -200,6 +214,41 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("1.1", 1.1)]
|
||||
[TestCase("-1.1", -1.1)]
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("null", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[TestCase("1E-2", 0.01)]
|
||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
public void TestDecimalConverterString(string value, decimal? expected)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
||||
}
|
||||
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("1.1", 1.1)]
|
||||
[TestCase("-1.1", -1.1)]
|
||||
[TestCase("null", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[TestCase("1E-2", 0.01)]
|
||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
public void TestDecimalConverterNumber(string value, decimal? expected)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
||||
}
|
||||
}
|
||||
|
||||
public class STJDecimalObject
|
||||
{
|
||||
[JsonConverter(typeof(DecimalConverter))]
|
||||
[JsonPropertyName("test")]
|
||||
public decimal? Test { get; set; }
|
||||
}
|
||||
|
||||
public class STJTimeObject
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class SubResponse
|
||||
{
|
||||
|
||||
[JsonProperty("action")]
|
||||
public string Action { get; set; } = null!;
|
||||
|
||||
[JsonProperty("channel")]
|
||||
public string Channel { get; set; } = null!;
|
||||
|
||||
@@ -19,6 +23,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
|
||||
internal class UnsubResponse
|
||||
{
|
||||
[JsonProperty("action")]
|
||||
public string Action { get; set; } = null!;
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; } = null!;
|
||||
}
|
||||
@@ -29,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
|
||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
ListenerIdentifiers = new HashSet<string> { channel };
|
||||
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
|
||||
}
|
||||
|
||||
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
|
||||
|
||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
||||
{
|
||||
|
||||
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
}
|
||||
|
||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, IDictionary<string, object> uriParams, IDictionary<string, object> bodyParams, Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
public string GetKey() => _credentials.Key;
|
||||
public string GetSecret() => _credentials.Secret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +1,132 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
//using System;
|
||||
//using System.IO;
|
||||
//using System.Net.WebSockets;
|
||||
//using System.Security.Authentication;
|
||||
//using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
//using CryptoExchange.Net.Interfaces;
|
||||
//using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocket: IWebsocket
|
||||
{
|
||||
public bool CanConnect { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
//{
|
||||
// public class TestSocket: IWebsocket
|
||||
// {
|
||||
// public bool CanConnect { get; set; }
|
||||
// public bool Connected { get; set; }
|
||||
|
||||
public event Func<Task> OnClose;
|
||||
#pragma warning disable 0067
|
||||
public event Func<Task> OnReconnected;
|
||||
public event Func<Task> OnReconnecting;
|
||||
public event Func<int, Task> OnRequestRateLimited;
|
||||
#pragma warning restore 0067
|
||||
public event Func<int, Task> OnRequestSent;
|
||||
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
|
||||
public event Func<Exception, Task> OnError;
|
||||
public event Func<Task> OnOpen;
|
||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
// public event Func<Task> OnClose;
|
||||
//#pragma warning disable 0067
|
||||
// public event Func<Task> OnReconnected;
|
||||
// public event Func<Task> OnReconnecting;
|
||||
// public event Func<int, Task> OnRequestRateLimited;
|
||||
//#pragma warning restore 0067
|
||||
// public event Func<int, Task> OnRequestSent;
|
||||
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||
// public event Func<Exception, Task> OnError;
|
||||
// public event Func<Task> OnOpen;
|
||||
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
|
||||
public int Id { get; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
public TimeSpan Timeout { get; set; }
|
||||
public Func<string, string> DataInterpreterString { get; set; }
|
||||
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
public string Url { get; }
|
||||
public bool IsClosed => !Connected;
|
||||
public bool IsOpen => Connected;
|
||||
public bool PingConnection { get; set; }
|
||||
public TimeSpan PingInterval { get; set; }
|
||||
public SslProtocols SSLProtocols { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
// public int Id { get; }
|
||||
// public bool ShouldReconnect { get; set; }
|
||||
// public TimeSpan Timeout { get; set; }
|
||||
// public Func<string, string> DataInterpreterString { get; set; }
|
||||
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
// public DateTime? DisconnectTime { get; set; }
|
||||
// public string Url { get; }
|
||||
// public bool IsClosed => !Connected;
|
||||
// public bool IsOpen => Connected;
|
||||
// public bool PingConnection { get; set; }
|
||||
// public TimeSpan PingInterval { get; set; }
|
||||
// public SslProtocols SSLProtocols { get; set; }
|
||||
// public Encoding Encoding { get; set; }
|
||||
|
||||
public int ConnectCalls { get; private set; }
|
||||
public bool Reconnecting { get; set; }
|
||||
public string Origin { get; set; }
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
// public int ConnectCalls { get; private set; }
|
||||
// public bool Reconnecting { get; set; }
|
||||
// public string Origin { get; set; }
|
||||
// public int? RatelimitPerSecond { get; set; }
|
||||
|
||||
public double IncomingKbps => throw new NotImplementedException();
|
||||
// public double IncomingKbps => throw new NotImplementedException();
|
||||
|
||||
public Uri Uri => new Uri("");
|
||||
// public Uri Uri => new Uri("");
|
||||
|
||||
public TimeSpan KeepAliveInterval { get; set; }
|
||||
// public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
// public static int lastId = 0;
|
||||
// public static object lastIdLock = new object();
|
||||
|
||||
public TestSocket()
|
||||
{
|
||||
lock (lastIdLock)
|
||||
{
|
||||
Id = lastId + 1;
|
||||
lastId++;
|
||||
}
|
||||
}
|
||||
// public TestSocket()
|
||||
// {
|
||||
// lock (lastIdLock)
|
||||
// {
|
||||
// Id = lastId + 1;
|
||||
// lastId++;
|
||||
// }
|
||||
// }
|
||||
|
||||
public Task<CallResult> ConnectAsync()
|
||||
{
|
||||
Connected = CanConnect;
|
||||
ConnectCalls++;
|
||||
if (CanConnect)
|
||||
InvokeOpen();
|
||||
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
}
|
||||
// public Task<CallResult> ConnectAsync()
|
||||
// {
|
||||
// Connected = CanConnect;
|
||||
// ConnectCalls++;
|
||||
// if (CanConnect)
|
||||
// InvokeOpen();
|
||||
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
// }
|
||||
|
||||
public void Send(int requestId, string data, int weight)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
}
|
||||
// public bool Send(int requestId, string data, int weight)
|
||||
// {
|
||||
// if(!Connected)
|
||||
// throw new Exception("Socket not connected");
|
||||
// OnRequestSent?.Invoke(requestId);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
// public void Reset()
|
||||
// {
|
||||
// }
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
OnClose?.Invoke();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
// public Task CloseAsync()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// OnClose?.Invoke();
|
||||
// return Task.FromResult(0);
|
||||
// }
|
||||
|
||||
public void SetProxy(string host, int port)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
// public void SetProxy(string host, int port)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
// public void Dispose()
|
||||
// {
|
||||
// }
|
||||
|
||||
public void InvokeClose()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
Reconnecting = true;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
// public void InvokeClose()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// Reconnecting = true;
|
||||
// OnClose?.Invoke();
|
||||
// }
|
||||
|
||||
public void InvokeOpen()
|
||||
{
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
// public void InvokeOpen()
|
||||
// {
|
||||
// OnOpen?.Invoke();
|
||||
// }
|
||||
|
||||
public void InvokeMessage(string data)
|
||||
{
|
||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
|
||||
}
|
||||
// public void InvokeMessage(string data)
|
||||
// {
|
||||
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
||||
// }
|
||||
|
||||
public void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
// public void SetProxy(ApiProxy proxy)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
public void InvokeError(Exception error)
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
public Task ReconnectAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
// public void InvokeError(Exception error)
|
||||
// {
|
||||
// OnError?.Invoke(error);
|
||||
// }
|
||||
// public Task ReconnectAsync() => Task.CompletedTask;
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -13,11 +13,11 @@ using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CryptoExchange.Net.Testing.Implementations;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
internal class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
@@ -41,12 +41,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
||||
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
||||
|
||||
public Subscription TestSubscription { get; private set; } = null;
|
||||
@@ -110,7 +111,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
var id = message.GetValue<string>(_channelPath);
|
||||
id ??= message.GetValue<string>(_topicPath);
|
||||
|
||||
return id;
|
||||
return message.GetValue<string>(_actionPath) + "-" + id;
|
||||
}
|
||||
|
||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
|
||||
@@ -9,48 +8,23 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials: IDisposable
|
||||
public class ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Key { get; }
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Secret { get; }
|
||||
public string Secret { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
@@ -72,8 +46,8 @@ namespace CryptoExchange.Net.Authentication
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,8 +56,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
return new ApiCredentials(Key, Secret, CredentialType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,19 +76,10 @@ namespace CryptoExchange.Net.Authentication
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
|
||||
inputStream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Secret?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Base class for authentication providers
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
public abstract class AuthenticationProvider
|
||||
{
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Get the API key of the current credentials
|
||||
/// </summary>
|
||||
public string ApiKey => _credentials.Key;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -38,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,9 +63,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
IDictionary<string, object> uriParameters,
|
||||
IDictionary<string, object> bodyParameters,
|
||||
Dictionary<string, string> headers,
|
||||
ref IDictionary<string, object>? uriParameters,
|
||||
ref IDictionary<string, object>? bodyParameters,
|
||||
ref Dictionary<string, string>? headers,
|
||||
bool auth,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
@@ -366,7 +371,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!.GetString()
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
@@ -381,7 +386,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||
{
|
||||
// Read from xml private key format
|
||||
rsa.FromXmlString(_credentials.Secret!.GetString());
|
||||
rsa.FromXmlString(_credentials.Secret!);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -447,12 +452,6 @@ namespace CryptoExchange.Net.Authentication
|
||||
else
|
||||
return serializer.Serialize(parameters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -65,10 +65,7 @@ namespace CryptoExchange.Net.Clients
|
||||
BaseAddress = baseAddress;
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,10 +82,7 @@ namespace CryptoExchange.Net.Clients
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
if (credentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -97,7 +91,6 @@ namespace CryptoExchange.Net.Clients
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,36 +196,40 @@ namespace CryptoExchange.Net.Clients
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null) where T : class
|
||||
{
|
||||
var key = baseAddress + definition + uriParameters?.ToFormData();
|
||||
string? cacheKey = null;
|
||||
if (ShouldCache(definition))
|
||||
{
|
||||
_logger.CheckingCache(key);
|
||||
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
|
||||
_logger.CheckingCache(cacheKey);
|
||||
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
||||
if (cachedValue != null)
|
||||
{
|
||||
_logger.CacheHit(key);
|
||||
_logger.CacheHit(cacheKey);
|
||||
var original = (WebCallResult<T>)cachedValue;
|
||||
return original.Cached();
|
||||
}
|
||||
|
||||
_logger.CacheNotHit(key);
|
||||
_logger.CacheNotHit(cacheKey);
|
||||
}
|
||||
|
||||
int currentTry = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var prepareResult = await PrepareAsync(baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
|
||||
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||
if (!prepareResult)
|
||||
return new WebCallResult<T>(prepareResult.Error!);
|
||||
|
||||
var request = CreateRequest(
|
||||
requestId,
|
||||
baseAddress,
|
||||
definition,
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
additionalHeaders);
|
||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||
TotalRequestsMade++;
|
||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||
if (!result)
|
||||
@@ -239,7 +243,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (result.Success &&
|
||||
ShouldCache(definition))
|
||||
{
|
||||
_cache.Add(key, result);
|
||||
_cache.Add(cacheKey!, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -249,6 +253,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
||||
/// </summary>
|
||||
/// <param name="requestId">Request id</param>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
@@ -257,13 +262,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
protected virtual async Task<CallResult> PrepareAsync(
|
||||
int requestId,
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null)
|
||||
{
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
var requestWeight = weight ?? definition.Weight;
|
||||
|
||||
// Time sync
|
||||
@@ -305,14 +310,14 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
// Endpoint specific rate limiting
|
||||
if (definition.EndpointLimitCount != null && definition.EndpointLimitPeriod != null)
|
||||
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
if (definition.RateLimitGate == null)
|
||||
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
||||
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult(limitResult.Error!);
|
||||
}
|
||||
@@ -324,6 +329,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Creates a request object
|
||||
/// </summary>
|
||||
/// <param name="requestId">Id of the request</param>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="uriParameters">The query parameters of the request</param>
|
||||
@@ -331,22 +337,22 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest CreateRequest(
|
||||
int requestId,
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? uriParameters,
|
||||
ParameterCollection? bodyParameters,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters);
|
||||
var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters);
|
||||
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
|
||||
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
|
||||
|
||||
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
||||
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
Dictionary<string, string>? headers = null;
|
||||
if (AuthenticationProvider != null)
|
||||
{
|
||||
try
|
||||
@@ -355,9 +361,9 @@ namespace CryptoExchange.Net.Clients
|
||||
this,
|
||||
uri,
|
||||
definition.Method,
|
||||
uriParams,
|
||||
bodyParams,
|
||||
headers,
|
||||
ref uriParams,
|
||||
ref bodyParams,
|
||||
ref headers,
|
||||
definition.Authenticated,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
@@ -370,14 +376,18 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||
uri = uri.SetParameters(uriParams, arraySerialization);
|
||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||
if (uriParams != null)
|
||||
uri = uri.SetParameters(uriParams, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
@@ -398,7 +408,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParams.Count != 0)
|
||||
if (bodyParams != null && bodyParams.Count != 0)
|
||||
WriteParamBody(request, bodyParams, contentType);
|
||||
else
|
||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||
@@ -668,47 +678,47 @@ namespace CryptoExchange.Net.Clients
|
||||
if (error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(object))
|
||||
// Success status code and expected empty response, assume it's correct
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||
|
||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||
if (!valid)
|
||||
{
|
||||
// Invalid json
|
||||
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
// Json response received
|
||||
var parsedError = TryParseError(accessor);
|
||||
if (parsedError != null)
|
||||
// Success status code, but TryParseError determined it was an error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||
|
||||
var deserializeResult = accessor.Deserialize<T>();
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -802,8 +812,8 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null;
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null;
|
||||
if (AuthenticationProvider != null)
|
||||
{
|
||||
try
|
||||
@@ -812,9 +822,9 @@ namespace CryptoExchange.Net.Clients
|
||||
this,
|
||||
uri,
|
||||
method,
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
headers,
|
||||
ref uriParameters,
|
||||
ref bodyParameters,
|
||||
ref headers,
|
||||
signed,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
@@ -827,24 +837,18 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||
{
|
||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||
$"should return provided parameters in either the uri or body parameters output");
|
||||
}
|
||||
}
|
||||
|
||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||
if (uriParameters != null)
|
||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(method, uri, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
@@ -865,7 +869,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
if (bodyParameters?.Any() == true)
|
||||
WriteParamBody(request, bodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||
@@ -962,14 +966,14 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
@@ -998,7 +1002,7 @@ namespace CryptoExchange.Net.Clients
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||
}
|
||||
|
||||
private bool ShouldCache(RequestDefinition definition)
|
||||
|
||||
@@ -189,7 +189,10 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
if (subscription.Authenticated && AuthenticationProvider == null)
|
||||
{
|
||||
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
@@ -251,7 +254,7 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
var waitEvent = new ManualResetEvent(false);
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
||||
if (subQuery != null)
|
||||
{
|
||||
@@ -269,7 +272,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||
await socketConnection.CloseAsync(subscription, isTimeout).ConfigureAwait(false);
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
}
|
||||
@@ -786,9 +789,10 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Preprocess a stream message
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||
/// </summary>
|
||||
public class BigDecimalConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
|
||||
return objectType == typeof(decimal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal; set it to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = reader.Value!.ToString();
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal; set it to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||
public Type? JsonConverterType { get; set; }
|
||||
public bool DefaultDeserialization { get; set; }
|
||||
public Type TargetType { get; set; } = null!;
|
||||
}
|
||||
|
||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
||||
@@ -70,7 +71,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
ArrayProperty = att,
|
||||
PropertyInfo = property,
|
||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType
|
||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
||||
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,10 +96,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||
if (attribute == null)
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetType = attribute.PropertyInfo.PropertyType;
|
||||
|
||||
var targetType = attribute.TargetType;
|
||||
object? value = null;
|
||||
if (attribute.JsonConverterType != null)
|
||||
{
|
||||
@@ -124,7 +128,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
};
|
||||
}
|
||||
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, attribute.PropertyInfo.PropertyType, CultureInfo.InvariantCulture));
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||
/// </summary>
|
||||
public class BigDecimalConverter : JsonConverter<decimal>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch(OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return reader.GetDecimal();
|
||||
}
|
||||
catch(FormatException)
|
||||
{
|
||||
// Format issue, assume value is too large
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
|
||||
return null;
|
||||
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch(OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
return reader.GetDecimal();
|
||||
try
|
||||
{
|
||||
return reader.GetDecimal();
|
||||
}
|
||||
catch(FormatException)
|
||||
{
|
||||
// Format issue, assume value is too large
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -211,5 +211,37 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the enum value from a string
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Enum type</typeparam>
|
||||
/// <param name="value">String value</param>
|
||||
/// <returns></returns>
|
||||
public static T? ParseString<T>(string value) where T : Enum
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (!_mapping.TryGetValue(type, out var enumMapping))
|
||||
enumMapping = AddMapping(type);
|
||||
|
||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
{
|
||||
return (T)mapping.Key;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
return (T)Enum.Parse(type, value, true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Int converter
|
||||
/// </summary>
|
||||
public class IntConverter : JsonConverter<int?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return reader.GetInt32();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Int converter
|
||||
/// </summary>
|
||||
public class LongConverter : JsonConverter<long?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return reader.GetInt64();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Read string or number as string
|
||||
/// </summary>
|
||||
public class NumberStringConverter : JsonConverter<string?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
if (reader.TryGetInt64(out var value))
|
||||
return value.ToString();
|
||||
|
||||
return reader.GetDecimal().ToString();
|
||||
}
|
||||
|
||||
return reader.GetString();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
return (T?)JsonDocument.Parse(value).Deserialize(typeof(T));
|
||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
new EnumConverter(),
|
||||
new BoolConverter(),
|
||||
new DecimalConverter(),
|
||||
new IntConverter(),
|
||||
new LongConverter()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,6 +123,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||
return default;
|
||||
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
if (value.Value.ValueKind == JsonValueKind.Number)
|
||||
return (T)(object)value.Value.GetInt64().ToString();
|
||||
}
|
||||
|
||||
return value.Value.Deserialize<T>();
|
||||
}
|
||||
|
||||
@@ -242,6 +248,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
|
||||
@@ -261,6 +268,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
try
|
||||
{
|
||||
var firstByte = data.Span[0];
|
||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||
{
|
||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("Not a json value"));
|
||||
}
|
||||
|
||||
_document = JsonDocument.Parse(data);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
@@ -289,6 +304,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>7.7.0</PackageVersion>
|
||||
<AssemblyVersion>7.7.0</AssemblyVersion>
|
||||
<FileVersion>7.7.0</FileVersion>
|
||||
<PackageVersion>7.11.1</PackageVersion>
|
||||
<AssemblyVersion>7.11.1</AssemblyVersion>
|
||||
<FileVersion>7.11.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -58,6 +58,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.3" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -15,10 +16,6 @@ namespace CryptoExchange.Net
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
private static int _lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
private static object _idLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a value between a min and max
|
||||
@@ -135,24 +132,13 @@ namespace CryptoExchange.Net
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int NextId()
|
||||
{
|
||||
lock (_idLock)
|
||||
{
|
||||
_lastId += 1;
|
||||
return _lastId;
|
||||
}
|
||||
}
|
||||
public static int NextId() => Interlocked.Increment(ref _lastId);
|
||||
|
||||
/// <summary>
|
||||
/// Return the last unique id that was generated
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int LastId()
|
||||
{
|
||||
lock (_idLock)
|
||||
return _lastId;
|
||||
}
|
||||
public static int LastId() => _lastId;
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
|
||||
@@ -113,92 +113,6 @@ namespace CryptoExchange.Net
|
||||
return formData.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string the secure string is representing
|
||||
/// </summary>
|
||||
/// <param name="source">The source secure string</param>
|
||||
/// <returns></returns>
|
||||
public static string GetString(this SecureString source)
|
||||
{
|
||||
lock (source)
|
||||
{
|
||||
string result;
|
||||
var length = source.Length;
|
||||
var pointer = IntPtr.Zero;
|
||||
var chars = new char[length];
|
||||
|
||||
try
|
||||
{
|
||||
pointer = Marshal.SecureStringToBSTR(source);
|
||||
Marshal.Copy(pointer, chars, 0, length);
|
||||
|
||||
result = string.Join("", chars);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pointer != IntPtr.Zero)
|
||||
{
|
||||
Marshal.ZeroFreeBSTR(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Are 2 secure strings equal
|
||||
/// </summary>
|
||||
/// <param name="ss1">Source secure string</param>
|
||||
/// <param name="ss2">Compare secure string</param>
|
||||
/// <returns>True if equal by value</returns>
|
||||
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
|
||||
{
|
||||
IntPtr bstr1 = IntPtr.Zero;
|
||||
IntPtr bstr2 = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
bstr1 = Marshal.SecureStringToBSTR(ss1);
|
||||
bstr2 = Marshal.SecureStringToBSTR(ss2);
|
||||
int length1 = Marshal.ReadInt32(bstr1, -4);
|
||||
int length2 = Marshal.ReadInt32(bstr2, -4);
|
||||
if (length1 == length2)
|
||||
{
|
||||
for (int x = 0; x < length1; ++x)
|
||||
{
|
||||
byte b1 = Marshal.ReadByte(bstr1, x);
|
||||
byte b2 = Marshal.ReadByte(bstr2, x);
|
||||
if (b1 != b2) return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
|
||||
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a secure string from a string
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <returns></returns>
|
||||
public static SecureString ToSecureString(this string source)
|
||||
{
|
||||
var secureString = new SecureString();
|
||||
foreach (var c in source)
|
||||
secureString.AppendChar(c);
|
||||
secureString.MakeReadOnly();
|
||||
return secureString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an int is one of the allowed values
|
||||
/// </summary>
|
||||
@@ -318,26 +232,6 @@ namespace CryptoExchange.Net
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
public static string FillPathParameters(this string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
@@ -453,7 +347,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using Gzip
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
@@ -467,6 +361,23 @@ namespace CryptoExchange.Net
|
||||
deflateStream.CopyTo(decompressedStream);
|
||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using DeflateStream
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
||||
{
|
||||
var output = new MemoryStream();
|
||||
|
||||
using (var compressStream = new MemoryStream(input.ToArray()))
|
||||
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
||||
decompressor.CopyTo(output);
|
||||
|
||||
output.Position = 0;
|
||||
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -25,7 +26,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
CallResult Handle(SocketConnection connection, DataEvent<object> message);
|
||||
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
||||
/// <summary>
|
||||
/// Get the type the message should be deserialized to
|
||||
/// </summary>
|
||||
|
||||
@@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="requestWeight">The weight of the request</param>
|
||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||
/// <returns>The time in milliseconds spend waiting</returns>
|
||||
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, string? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Websocket message received event
|
||||
/// </summary>
|
||||
event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
|
||||
event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||
/// <summary>
|
||||
/// Websocket sent event, RequestId as parameter
|
||||
/// </summary>
|
||||
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="weight"></param>
|
||||
void Send(int id, string data, int weight);
|
||||
bool Send(int id, string data, int weight);
|
||||
/// <summary>
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
|
||||
+28
-2
@@ -3,7 +3,8 @@ using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
internal static class CryptoExchangeWebSocketClientLoggingExtension
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static class CryptoExchangeWebSocketClientLoggingExtension
|
||||
{
|
||||
private static readonly Action<ILogger, int, Exception?> _connecting;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
|
||||
@@ -24,6 +25,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
|
||||
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
|
||||
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
|
||||
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseConfirmation;
|
||||
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
|
||||
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
|
||||
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
|
||||
@@ -32,6 +34,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
|
||||
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
|
||||
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
|
||||
|
||||
static CryptoExchangeWebSocketClientLoggingExtension()
|
||||
{
|
||||
@@ -151,7 +154,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
|
||||
|
||||
_receiveLoopStoppedWithException = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
LogLevel.Error,
|
||||
new EventId(1024, "ReceiveLoopStoppedWithException"),
|
||||
"[Sckt {SocketId}] receive loop stopped with exception");
|
||||
|
||||
@@ -169,6 +172,17 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Debug,
|
||||
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
|
||||
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
|
||||
|
||||
_receivedCloseConfirmation = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1028, "ReceivedCloseMessage"),
|
||||
"[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
|
||||
|
||||
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1028, "SocketProcessingStateChanged"),
|
||||
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
|
||||
|
||||
}
|
||||
|
||||
public static void SocketConnecting(
|
||||
@@ -285,6 +299,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
|
||||
}
|
||||
|
||||
public static void SocketReceivedCloseConfirmation(
|
||||
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
|
||||
{
|
||||
_receivedCloseConfirmation(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
|
||||
}
|
||||
|
||||
public static void SocketReceivedPartialMessage(
|
||||
this ILogger logger, int socketId, int countBytes)
|
||||
{
|
||||
@@ -332,5 +352,11 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
|
||||
}
|
||||
|
||||
public static void SocketProcessingStateChanged(
|
||||
this ILogger logger, int socketId, string prevState, string newState)
|
||||
{
|
||||
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
internal static class RateLimitGateLoggingExtensions
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static class RateLimitGateLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
|
||||
|
||||
@@ -6,7 +6,8 @@ using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
internal static class RestApiClientLoggingExtensions
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static class RestApiClientLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
|
||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
||||
|
||||
@@ -3,7 +3,8 @@ using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
internal static class SocketApiClientLoggingExtension
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static class SocketApiClientLoggingExtension
|
||||
{
|
||||
private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection;
|
||||
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment;
|
||||
|
||||
@@ -4,7 +4,8 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
internal static class SocketConnectionLoggingExtension
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static class SocketConnectionLoggingExtension
|
||||
{
|
||||
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
|
||||
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
|
||||
|
||||
@@ -4,7 +4,9 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
internal static class SymbolOrderBookLoggingExtensions
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
public static class SymbolOrderBookLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged;
|
||||
private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting;
|
||||
|
||||
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The password of the proxy
|
||||
/// </summary>
|
||||
public SecureString? Password { get; }
|
||||
public string? 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): this(host, port, null, (SecureString?)null)
|
||||
public ApiProxy(string host, int port): this(host, port, null, null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -42,18 +42,7 @@ 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, login, password?.ToSecureString())
|
||||
{
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public ApiProxy(string host, int port, string? login, string? password)
|
||||
{
|
||||
Host = host;
|
||||
Port = port;
|
||||
|
||||
@@ -273,6 +273,28 @@ namespace CryptoExchange.Net.Objects
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="data">The data of the new type</param>
|
||||
/// <returns></returns>
|
||||
public WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -349,6 +371,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
/// <param name="requestHeaders"></param>
|
||||
/// <param name="dataSource"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
@@ -362,6 +385,7 @@ namespace CryptoExchange.Net.Objects
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||
ResultDataSource dataSource,
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, originalData, error)
|
||||
{
|
||||
@@ -375,6 +399,7 @@ namespace CryptoExchange.Net.Objects
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
DataSource = dataSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -398,7 +423,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
@@ -408,7 +433,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -419,7 +444,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -428,9 +453,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
internal WebCallResult<T> Cached()
|
||||
{
|
||||
var result = new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, Error);
|
||||
result.DataSource = ResultDataSource.Cache;
|
||||
return result;
|
||||
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -149,6 +149,27 @@ namespace CryptoExchange.Net.Objects
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as string seconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddSecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as string seconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalSecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||
/// </summary>
|
||||
@@ -167,7 +188,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public void AddEnumAsInt<T>(string key, T value)
|
||||
{
|
||||
var stringVal = EnumConverter.GetString(value);
|
||||
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
|
||||
Add(key, int.Parse(stringVal)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -48,18 +48,16 @@ namespace CryptoExchange.Net.Objects
|
||||
/// Request weight
|
||||
/// </summary>
|
||||
public int Weight { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit gate to use
|
||||
/// </summary>
|
||||
public IRateLimitGate? RateLimitGate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit for this specific endpoint
|
||||
/// Individual endpoint rate limit guard to use
|
||||
/// </summary>
|
||||
public int? EndpointLimitCount { get; set; }
|
||||
/// <summary>
|
||||
/// Rate limit period for this specific endpoint
|
||||
/// </summary>
|
||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
||||
public IRateLimitGuard? LimitGuard { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -41,8 +41,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="method">The HttpMethod</param>
|
||||
/// <param name="path">Endpoint path</param>
|
||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||
/// <param name="endpointLimitCount">The limit count for this specific endpoint</param>
|
||||
/// <param name="endpointLimitPeriod">The period for the limit for this specific endpoint</param>
|
||||
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
|
||||
/// <param name="weight">Request weight</param>
|
||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||
/// <param name="requestBodyFormat">Request body format</param>
|
||||
@@ -56,8 +55,7 @@ namespace CryptoExchange.Net.Objects
|
||||
IRateLimitGate? rateLimitGate,
|
||||
int weight,
|
||||
bool authenticated,
|
||||
int? endpointLimitCount = null,
|
||||
TimeSpan? endpointLimitPeriod = null,
|
||||
IRateLimitGuard? limitGuard = null,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
@@ -69,8 +67,7 @@ namespace CryptoExchange.Net.Objects
|
||||
def = new RequestDefinition(path, method)
|
||||
{
|
||||
Authenticated = authenticated,
|
||||
EndpointLimitCount = endpointLimitCount,
|
||||
EndpointLimitPeriod = endpointLimitPeriod,
|
||||
LimitGuard = limitGuard,
|
||||
RateLimitGate = rateLimitGate,
|
||||
Weight = weight,
|
||||
ArraySerialization = arraySerialization,
|
||||
|
||||
@@ -144,5 +144,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (lastUpdateId <= LastSequenceNumber)
|
||||
{
|
||||
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
|
||||
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> definition.Authenticated == _authenticated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> _paths.Contains(definition.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> host == _host;
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> type == _type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,26 +14,26 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <summary>
|
||||
/// Apply guard per host
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
|
||||
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host);
|
||||
/// <summary>
|
||||
/// Apply guard per endpoint
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
|
||||
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
/// <summary>
|
||||
/// Apply guard per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
|
||||
/// <summary>
|
||||
/// Apply guard per API key per endpoint
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method);
|
||||
|
||||
private readonly IEnumerable<IGuardFilter> _filters;
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private RateLimitWindowType _windowType;
|
||||
private double? _decayRate;
|
||||
private int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
|
||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "RateLimitGuard";
|
||||
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
||||
{
|
||||
}
|
||||
@@ -75,7 +75,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||
{
|
||||
foreach(var filter in _filters)
|
||||
{
|
||||
@@ -114,7 +114,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||
{
|
||||
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
||||
if (dif <= TimeSpan.Zero)
|
||||
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||
{
|
||||
return RateLimitState.NotApplied;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,22 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// </summary>
|
||||
public class SingleLimitGuard : IRateLimitGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Default endpoint limit
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint limit per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly int _limit;
|
||||
private readonly TimeSpan _period;
|
||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "EndpointLimitGuard";
|
||||
@@ -25,20 +38,28 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
|
||||
public SingleLimitGuard(
|
||||
int limit,
|
||||
TimeSpan period,
|
||||
RateLimitWindowType windowType,
|
||||
double? decayRate = null,
|
||||
Func<RequestDefinition, string, string?, string>? keySelector = null)
|
||||
{
|
||||
_limit = limit;
|
||||
_period = period;
|
||||
_windowType = windowType;
|
||||
_decayRate = decayRate;
|
||||
_keySelector = keySelector ?? Default;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||
{
|
||||
var key = definition.Path + definition.Method;
|
||||
var key = _keySelector(definition, host, apiKey);
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
|
||||
tracker = CreateTracker();
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
@@ -46,27 +67,27 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
|
||||
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
||||
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||
{
|
||||
var key = definition.Path + definition.Method;
|
||||
var key = _keySelector(definition, host, apiKey);
|
||||
var tracker = _trackers[key];
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
||||
return RateLimitState.Applied(_limit, _period, tracker.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new WindowTracker
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
|
||||
protected IWindowTracker CreateTracker()
|
||||
{
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
|
||||
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
|
||||
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="host">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <returns>True if passed</returns>
|
||||
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
|
||||
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <returns></returns>
|
||||
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
||||
|
||||
/// <summary>
|
||||
/// Set the SingleLimitGuard for handling individual endpoint rate limits
|
||||
/// </summary>
|
||||
/// <param name="guard"></param>
|
||||
/// <returns></returns>
|
||||
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the 'retry after' timestamp if set
|
||||
/// </summary>
|
||||
@@ -58,21 +51,21 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="itemId">Id of the item to check</param>
|
||||
/// <param name="guard">The guard</param>
|
||||
/// <param name="type">The rate limit item type</param>
|
||||
/// <param name="definition">The request definition</param>
|
||||
/// <param name="baseAddress">The host address</param>
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">Request weight</param>
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <returns></returns>
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight);
|
||||
|
||||
/// <summary>
|
||||
/// Apply the request to this guard with the specified weight
|
||||
@@ -39,6 +39,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <returns></returns>
|
||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// <inheritdoc />
|
||||
public class RateLimitGate : IRateLimitGate
|
||||
{
|
||||
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
|
||||
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private readonly string _name;
|
||||
@@ -37,7 +36,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
_waitingCount++;
|
||||
@@ -53,16 +52,23 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
public async Task<CallResult> ProcessSingleAsync(
|
||||
ILogger logger,
|
||||
int itemId,
|
||||
IRateLimitGuard guard,
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string host,
|
||||
string? apiKey,
|
||||
RateLimitingBehaviour rateLimitingBehaviour,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
if (requestWeight == 0)
|
||||
requestWeight = 1;
|
||||
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -71,7 +77,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||
{
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
@@ -130,13 +136,6 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
|
||||
{
|
||||
_singleLimitGuard = guard;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
||||
{
|
||||
|
||||
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||
{
|
||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private TimeSpan DetermineWaitTime()
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
var checkTime = DateTime.UtcNow;
|
||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||
return wait.Add(_fixedWindowBuffer);
|
||||
var result = wait.Add(_fixedWindowBuffer);
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private readonly List<LimitEntry> _entries;
|
||||
private int _currentWeight = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for fluctuating request times
|
||||
/// </summary>
|
||||
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
|
||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||
{
|
||||
Limit = limit;
|
||||
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
removedWeight += entry.Weight;
|
||||
if (removedWeight >= weightToRemove)
|
||||
{
|
||||
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
|
||||
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
public event Func<Task>? OnClose;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>>? OnStreamMessage;
|
||||
public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task>? OnStreamMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<int, Task>? OnRequestSent;
|
||||
@@ -233,30 +233,31 @@ namespace CryptoExchange.Net.Sockets
|
||||
while (!_stopRequested)
|
||||
{
|
||||
_logger.SocketStartingProcessing(Id);
|
||||
_processState = ProcessState.Processing;
|
||||
SetProcessState(ProcessState.Processing);
|
||||
var sendTask = SendLoopAsync();
|
||||
var receiveTask = ReceiveLoopAsync();
|
||||
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||
_logger.SocketFinishedProcessing(Id);
|
||||
|
||||
_processState = ProcessState.WaitingForClose;
|
||||
SetProcessState(ProcessState.WaitingForClose);
|
||||
while (_closeTask == null)
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
_closeTask = null;
|
||||
if (!_stopRequested)
|
||||
_closeTask = null;
|
||||
|
||||
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
|
||||
{
|
||||
_processState = ProcessState.Idle;
|
||||
SetProcessState(ProcessState.Idle);
|
||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_stopRequested)
|
||||
{
|
||||
_processState = ProcessState.Reconnecting;
|
||||
SetProcessState(ProcessState.Reconnecting);
|
||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -295,12 +296,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
_reconnectAttempt = 0;
|
||||
_lastReconnectTime = DateTime.UtcNow;
|
||||
|
||||
// Set to processing before reconnect handling
|
||||
SetProcessState(ProcessState.Processing);
|
||||
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_processState = ProcessState.Idle;
|
||||
SetProcessState(ProcessState.Idle);
|
||||
}
|
||||
|
||||
private TimeSpan GetReconnectDelay()
|
||||
@@ -322,15 +326,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Send(int id, string data, int weight)
|
||||
public virtual bool Send(int id, string data, int weight)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
return;
|
||||
if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing)
|
||||
return false;
|
||||
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||
_sendEvent.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -389,36 +394,33 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
//_closeState = CloseState.Closing;
|
||||
_ctsSource.Cancel();
|
||||
_sendEvent.Set();
|
||||
|
||||
if (_socket.State == WebSocketState.Open)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||
// So socket might go to aborted state, might still be open
|
||||
}
|
||||
}
|
||||
else if(_socket.State == WebSocketState.CloseReceived)
|
||||
{
|
||||
try
|
||||
if (_socket.State == WebSocketState.CloseReceived)
|
||||
{
|
||||
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
else if (_socket.State == WebSocketState.Open)
|
||||
{
|
||||
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||
// So socket might go to aborted state, might still be open
|
||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||
var startWait = DateTime.UtcNow;
|
||||
while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
|
||||
{
|
||||
// Wait until we receive close confirmation
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5))
|
||||
break; // Wait for max 5 seconds, then just abort the connection
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||
// So socket might go to aborted state, might still be open
|
||||
}
|
||||
|
||||
_ctsSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -436,6 +438,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
_disposed = true;
|
||||
_socket.Dispose();
|
||||
_ctsSource?.Dispose();
|
||||
_sendEvent.Dispose();
|
||||
_logger.SocketDisposed(Id);
|
||||
}
|
||||
|
||||
@@ -450,10 +453,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
try
|
||||
{
|
||||
if (!_sendBuffer.Any())
|
||||
await _sendEvent.WaitAsync(ct: _ctsSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
|
||||
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
break;
|
||||
@@ -507,7 +515,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Make sure we at least let the owner know there was an error
|
||||
_logger.SocketSendLoopStoppedWithException(Id, e.Message, e);
|
||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
throw;
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -558,10 +567,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
// Connection closed
|
||||
if (_socket.State == WebSocketState.CloseReceived)
|
||||
{
|
||||
// Close received means it server initiated, we should send a confirmation and close the socket
|
||||
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Means the socket is now closed and we were the one initiating it
|
||||
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -582,7 +601,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Received a complete message and it's not multi part
|
||||
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
|
||||
ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array, buffer.Offset, receiveResult.Count));
|
||||
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -617,7 +636,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
_logger.SocketReassembledMessage(Id, multipartStream!.Length);
|
||||
// Get the underlying buffer of the memorystream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
|
||||
ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length));
|
||||
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -633,7 +652,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Make sure we at least let the owner know there was an error
|
||||
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
throw;
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -647,10 +667,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="type"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected void ProcessData(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||
protected async Task ProcessData(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||
{
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
OnStreamMessage?.Invoke(type, data);
|
||||
await (OnStreamMessage?.Invoke(type, data) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -691,7 +711,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
||||
// Make sure we at least let the owner know there was an error
|
||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,10 +735,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
var checkTime = DateTime.UtcNow;
|
||||
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
|
||||
{
|
||||
foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list
|
||||
for (var i = 0; i < _receivedMessages.Count; i++)
|
||||
{
|
||||
var msg = _receivedMessages[i];
|
||||
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
|
||||
{
|
||||
_receivedMessages.Remove(msg);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
_lastReceivedMessagesUpdate = checkTime;
|
||||
@@ -747,6 +770,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (proxy.Login != null)
|
||||
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||
}
|
||||
|
||||
private void SetProcessState(ProcessState state)
|
||||
{
|
||||
if (_processState == state)
|
||||
return;
|
||||
|
||||
_logger.SocketProcessingStateChanged(Id, _processState.ToString(), state.ToString());
|
||||
_processState = state;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,6 +24,17 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public bool Completed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
|
||||
/// and each symbol receives it's own confirmation response
|
||||
/// </summary>
|
||||
public int RequiredResponses { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The current number of responses received on this query
|
||||
/// </summary>
|
||||
public int CurrentResponses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of when the request was send
|
||||
/// </summary>
|
||||
@@ -42,7 +53,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Wait event for the calling message processing thread
|
||||
/// </summary>
|
||||
public ManualResetEvent? ContinueAwaiter { get; set; }
|
||||
public AsyncResetEvent? ContinueAwaiter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Strings to match this query to a received message
|
||||
@@ -108,7 +119,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait untill timeout or the request is competed
|
||||
/// Wait until timeout or the request is completed
|
||||
/// </summary>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
@@ -135,7 +146,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="message"></param>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
public abstract CallResult Handle(SocketConnection connection, DataEvent<object> message);
|
||||
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
||||
|
||||
}
|
||||
|
||||
@@ -165,13 +176,26 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override CallResult Handle(SocketConnection connection, DataEvent<object> message)
|
||||
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
Completed = true;
|
||||
Response = message.Data;
|
||||
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
|
||||
_event.Set();
|
||||
ContinueAwaiter?.WaitOne();
|
||||
CurrentResponses++;
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
Completed = true;
|
||||
Response = message.Data;
|
||||
}
|
||||
|
||||
if (Result?.Success != false)
|
||||
// If an error result is already set don't override that
|
||||
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
|
||||
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
_event.Set();
|
||||
if (ContinueAwaiter != null)
|
||||
await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
|
||||
@@ -413,14 +413,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="data"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual void HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var receiveTime = DateTime.UtcNow;
|
||||
string? originalData = null;
|
||||
|
||||
// 1. Decrypt/Preprocess if necessary
|
||||
data = ApiClient.PreprocessStreamMessage(type, data);
|
||||
data = ApiClient.PreprocessStreamMessage(this, type, data);
|
||||
|
||||
// 2. Read data into accessor
|
||||
_accessor.Read(data);
|
||||
@@ -507,7 +507,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
try
|
||||
{
|
||||
var innerSw = Stopwatch.StartNew();
|
||||
processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null));
|
||||
await processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null)).ConfigureAwait(false);
|
||||
if (processor is Query query && query.RequiredResponses != 1)
|
||||
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
|
||||
totalUserTime += (int)innerSw.ElapsedMilliseconds;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -573,9 +575,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
|
||||
/// </summary>
|
||||
/// <param name="subscription">Subscription to close</param>
|
||||
/// <param name="unsubEvenIfNotConfirmed">Whether to send an unsub request even if the subscription wasn't confirmed</param>
|
||||
/// <returns></returns>
|
||||
public async Task CloseAsync(Subscription subscription, bool unsubEvenIfNotConfirmed = false)
|
||||
public async Task CloseAsync(Subscription subscription)
|
||||
{
|
||||
subscription.Closed = true;
|
||||
|
||||
@@ -589,14 +590,18 @@ namespace CryptoExchange.Net.Sockets
|
||||
bool anyDuplicateSubscription;
|
||||
lock (_listenersLock)
|
||||
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l)));
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (_listenersLock)
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
{
|
||||
bool needUnsub;
|
||||
lock (_listenersLock)
|
||||
needUnsub = _listeners.Contains(subscription);
|
||||
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
||||
|
||||
if (needUnsub && (unsubEvenIfNotConfirmed || subscription.Confirmed) && _socket.IsOpen)
|
||||
if (needUnsub && _socket.IsOpen)
|
||||
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -610,16 +615,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (_listenersLock)
|
||||
{
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||
if (shouldCloseConnection)
|
||||
Status = SocketStatus.Closing;
|
||||
}
|
||||
|
||||
if (shouldCloseConnection)
|
||||
{
|
||||
Status = SocketStatus.Closing;
|
||||
_logger.ClosingNoMoreSubscriptions(SocketId);
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
@@ -697,7 +695,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||
{
|
||||
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
||||
@@ -712,13 +710,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||
{
|
||||
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
|
||||
}
|
||||
|
||||
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent, CancellationToken ct = default)
|
||||
private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default)
|
||||
{
|
||||
lock(_listenersLock)
|
||||
_listeners.Add(query);
|
||||
@@ -802,7 +800,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
_logger.SendingData(SocketId, requestId, data);
|
||||
try
|
||||
{
|
||||
_socket.Send(requestId, data, weight);
|
||||
if (!_socket.Send(requestId, data, weight))
|
||||
return new CallResult(new WebError("Failed to send message, connection not open"));
|
||||
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch(Exception ex)
|
||||
@@ -832,7 +832,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
bool anyAuthenticated;
|
||||
lock (_listenersLock)
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
|
||||
{
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||
|| (DedicatedRequestConnection && ApiClient.AuthenticationProvider != null);
|
||||
}
|
||||
|
||||
if (anyAuthenticated)
|
||||
{
|
||||
// If we reconnected a authenticated connection we need to re-authenticate
|
||||
@@ -847,36 +851,37 @@ namespace CryptoExchange.Net.Sockets
|
||||
_logger.AuthenticationSucceeded(SocketId);
|
||||
}
|
||||
|
||||
// Get a list of all subscriptions on the socket
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().ToList();
|
||||
|
||||
foreach(var subscription in subList)
|
||||
{
|
||||
subscription.ConnectionInvocations = 0;
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
||||
for (var i = 0; i < subList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
||||
int batch = 0;
|
||||
int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||
while (true)
|
||||
{
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket not connected"));
|
||||
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
|
||||
if (subList.Count == 0)
|
||||
break;
|
||||
|
||||
var taskList = new List<Task<CallResult>>();
|
||||
foreach (var subscription in subList.Skip(i).Take(ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
||||
foreach (var subscription in subList)
|
||||
{
|
||||
subscription.ConnectionInvocations = 0;
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||
return result;
|
||||
}
|
||||
|
||||
var subQuery = subscription.GetSubQuery(this);
|
||||
if (subQuery == null)
|
||||
continue;
|
||||
|
||||
var waitEvent = new ManualResetEvent(false);
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||
{
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
@@ -890,6 +895,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||
if (taskList.Any(t => !t.Result.Success))
|
||||
return taskList.First(t => !t.Result.Success).Result;
|
||||
|
||||
batch++;
|
||||
}
|
||||
|
||||
if (!_socket.IsOpen)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
@@ -122,11 +123,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
public CallResult Handle(SocketConnection connection, DataEvent<object> message)
|
||||
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
ConnectionInvocations++;
|
||||
TotalInvocations++;
|
||||
return DoHandleMessage(connection, message);
|
||||
return Task.FromResult(DoHandleMessage(connection, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,7 +29,12 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
var nested = nestedJsonProperty.Split('.');
|
||||
foreach (var nest in nested)
|
||||
jsonObject = jsonObject![nest];
|
||||
{
|
||||
if (int.TryParse(nest, out var index))
|
||||
jsonObject = jsonObject![index];
|
||||
else
|
||||
jsonObject = jsonObject![nest];
|
||||
}
|
||||
}
|
||||
|
||||
if (userSingleArrayItem)
|
||||
@@ -80,6 +85,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
else if (jObj.Type == JTokenType.Array)
|
||||
{
|
||||
var resultObj = enumerator.Current;
|
||||
if (resultObj is string)
|
||||
// string list
|
||||
continue;
|
||||
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
@@ -88,9 +97,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (var item in jObj.Values())
|
||||
foreach (var item in jObj.Children())
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
@@ -108,9 +117,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
int i = 0;
|
||||
foreach (var item in jObjs.Values())
|
||||
foreach (var item in jObjs.Children())
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
@@ -224,11 +233,11 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (var item in jtoken.Values())
|
||||
foreach (var item in jtoken.Children())
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
|
||||
i++;
|
||||
}
|
||||
@@ -266,7 +275,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
var enumerator = list.GetEnumerator();
|
||||
foreach (var jObj in jObjs)
|
||||
{
|
||||
enumerator.MoveNext();
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
}
|
||||
|
||||
if (jObj.Type == JTokenType.Object)
|
||||
{
|
||||
foreach (var subProp in ((JObject)jObj).Properties())
|
||||
@@ -307,9 +319,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
int i = 0;
|
||||
foreach (var item in jObjs.Values())
|
||||
foreach (var item in jObjs.Children())
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
|
||||
@@ -26,8 +26,13 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
if (nestedJsonProperty != null)
|
||||
{
|
||||
var nested = nestedJsonProperty.Split('.');
|
||||
foreach(var nest in nested)
|
||||
jsonObject = jsonObject![nest];
|
||||
foreach (var nest in nested)
|
||||
{
|
||||
if (int.TryParse(nest, out var index))
|
||||
jsonObject = jsonObject![index];
|
||||
else
|
||||
jsonObject = jsonObject![nest];
|
||||
}
|
||||
}
|
||||
|
||||
if (userSingleArrayItem)
|
||||
@@ -65,44 +70,65 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
else if (jsonObject!.Type == JTokenType.Array)
|
||||
{
|
||||
var jObjs = (JArray)jsonObject;
|
||||
var list = (IEnumerable)resultData;
|
||||
var enumerator = list.GetEnumerator();
|
||||
foreach (var jObj in jObjs)
|
||||
if (resultData is IEnumerable list)
|
||||
{
|
||||
enumerator.MoveNext();
|
||||
if (jObj.Type == JTokenType.Object)
|
||||
var enumerator = list.GetEnumerator();
|
||||
foreach (var jObj in jObjs)
|
||||
{
|
||||
foreach (var subProp in ((JObject)jObj).Properties())
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||
continue;
|
||||
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||
}
|
||||
}
|
||||
else if (jObj.Type == JTokenType.Array)
|
||||
{
|
||||
var resultObj = enumerator.Current;
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
if (jsonConverter != typeof(ArrayConverter))
|
||||
// Not array converter?
|
||||
continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (var item in jObj.Values())
|
||||
if (jObj.Type == JTokenType.Object)
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
foreach (var subProp in ((JObject)jObj).Properties())
|
||||
{
|
||||
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||
continue;
|
||||
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||
}
|
||||
}
|
||||
else if (jObj.Type == JTokenType.Array)
|
||||
{
|
||||
var resultObj = enumerator.Current;
|
||||
if (resultObj is string)
|
||||
// string list
|
||||
continue;
|
||||
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
if (jsonConverter != typeof(ArrayConverter))
|
||||
// Not array converter?
|
||||
continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (var item in jObj.Children())
|
||||
{
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = enumerator.Current;
|
||||
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
int i = 0;
|
||||
foreach (var item in jObjs.Children())
|
||||
{
|
||||
var value = enumerator.Current;
|
||||
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,17 +151,35 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
|
||||
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||
{
|
||||
var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||
var publicProperties = obj.GetType().GetProperties(
|
||||
System.Reflection.BindingFlags.Public
|
||||
| System.Reflection.BindingFlags.GetProperty
|
||||
| System.Reflection.BindingFlags.SetProperty
|
||||
| System.Reflection.BindingFlags.Instance).Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||
|
||||
var internalProperties = obj.GetType().GetProperties(
|
||||
System.Reflection.BindingFlags.NonPublic
|
||||
| System.Reflection.BindingFlags.GetProperty
|
||||
| System.Reflection.BindingFlags.SetProperty
|
||||
| System.Reflection.BindingFlags.Instance)
|
||||
.Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(JsonIncludeAttribute)))
|
||||
.Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||
|
||||
var resultProperties = publicProperties.Concat(internalProperties);
|
||||
|
||||
// Property has a value
|
||||
var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p;
|
||||
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
|
||||
|
||||
if (property is null)
|
||||
// Property not found
|
||||
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||
|
||||
var getMethod = property.GetGetMethod();
|
||||
if (getMethod is null)
|
||||
// There is no getter, so probably just a set for an alternative json name
|
||||
return;
|
||||
|
||||
var propertyValue = property.GetValue(obj);
|
||||
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||
}
|
||||
@@ -215,9 +259,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (var item in jtoken.Values())
|
||||
foreach (var item in jtoken.Children())
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||
|
||||
@@ -249,6 +293,67 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (propValue.Type == JTokenType.Array)
|
||||
{
|
||||
var jObjs = (JArray)propValue;
|
||||
if (propertyValue is IEnumerable list)
|
||||
{
|
||||
var enumerator = list.GetEnumerator();
|
||||
foreach (var jObj in jObjs)
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
}
|
||||
|
||||
if (jObj.Type == JTokenType.Object)
|
||||
{
|
||||
foreach (var subProp in ((JObject)jObj).Properties())
|
||||
{
|
||||
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||
continue;
|
||||
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||
}
|
||||
}
|
||||
else if (jObj.Type == JTokenType.Array)
|
||||
{
|
||||
var resultObj = enumerator.Current;
|
||||
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||
if (jsonConverter != typeof(ArrayConverter))
|
||||
// Not array converter?
|
||||
continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (var item in jObj.Values())
|
||||
{
|
||||
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = enumerator.Current;
|
||||
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||
int i = 0;
|
||||
foreach (var item in jObjs.Children())
|
||||
{
|
||||
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||
if (arrayProp != null)
|
||||
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||
@@ -270,6 +375,14 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
|
||||
}
|
||||
else if (objectValue is bool bl)
|
||||
{
|
||||
var jsonStr = jsonValue.Value<string>();
|
||||
if (bl && (jsonStr != "1" && jsonStr != "true" && jsonStr != "True"))
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {bl}");
|
||||
if (!bl && (jsonStr != "0" && jsonStr != "-1" && jsonStr != "false" && jsonStr != "False"))
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {bl}");
|
||||
}
|
||||
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||
{
|
||||
// TODO enum comparing
|
||||
@@ -284,7 +397,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
if (objectValue is DateTime time)
|
||||
{
|
||||
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||
throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!)} vs {time}");
|
||||
}
|
||||
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||
{
|
||||
|
||||
@@ -9,12 +9,18 @@ namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
if (message.Contains("Cannot map"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
if (message.Contains("Received null enum value"))
|
||||
throw new Exception("Enum null error: " + message);
|
||||
}
|
||||
|
||||
public override void WriteLine(string message)
|
||||
{
|
||||
if (message.Contains("Cannot map"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
if (message.Contains("Received null enum value"))
|
||||
throw new Exception("Enum null error: " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
public event Func<Exception, Task>? OnError;
|
||||
#pragma warning restore 0067
|
||||
public event Func<int, Task>? OnRequestSent;
|
||||
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>>? OnStreamMessage;
|
||||
public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task>? OnStreamMessage;
|
||||
public event Func<Task>? OnOpen;
|
||||
|
||||
public int Id { get; }
|
||||
@@ -33,9 +33,17 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
public Uri Uri { get; set; }
|
||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
|
||||
public TestSocket(string address)
|
||||
{
|
||||
Uri = new Uri(address);
|
||||
lock (lastIdLock)
|
||||
{
|
||||
Id = lastId + 1;
|
||||
lastId++;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<CallResult> ConnectAsync()
|
||||
@@ -44,13 +52,14 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
}
|
||||
|
||||
public void Send(int requestId, string data, int weight)
|
||||
public bool Send(int requestId, string data, int weight)
|
||||
{
|
||||
if (!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
OnMessageSend?.Invoke(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task CloseAsync()
|
||||
@@ -72,12 +81,12 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
|
||||
public void InvokeMessage(string data)
|
||||
{
|
||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
|
||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
||||
}
|
||||
|
||||
public void InvokeMessage<T>(T data)
|
||||
{
|
||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data))));
|
||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)))).Wait();
|
||||
}
|
||||
|
||||
public Task ReconnectAsync() => throw new NotImplementedException();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for executing REST API integration tests
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient">Client type</typeparam>
|
||||
public abstract class RestIntergrationTest<TClient>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a client instance
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory"></param>
|
||||
/// <returns></returns>
|
||||
public abstract TClient GetClient(ILoggerFactory loggerFactory);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
||||
/// </summary>
|
||||
public virtual bool Run { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation.
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected TClient CreateClient()
|
||||
{
|
||||
var fact = new LoggerFactory();
|
||||
fact.AddProvider(new TraceLoggerProvider());
|
||||
return GetClient(fact);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if integration tests should be executed
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected bool ShouldRun()
|
||||
{
|
||||
var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION");
|
||||
if (!Run && integrationTests != "1")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a REST endpoint call and check for any errors or warnings.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of response</typeparam>
|
||||
/// <param name="expression">The call expression</param>
|
||||
/// <param name="authRequest">Whether this is an authenticated request</param>
|
||||
public async Task RunAndCheckResult<T>(Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest)
|
||||
{
|
||||
if (!ShouldRun())
|
||||
return;
|
||||
|
||||
var client = CreateClient();
|
||||
|
||||
var expressionBody = (MethodCallExpression)expression.Body;
|
||||
if (authRequest && !Authenticated)
|
||||
{
|
||||
Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
|
||||
WebCallResult<T> result;
|
||||
try
|
||||
{
|
||||
result = await expression.Compile().Invoke(client).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
|
||||
if (!result.Success)
|
||||
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
||||
|
||||
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,8 +121,8 @@ namespace CryptoExchange.Net.Testing
|
||||
if (disableOrdering)
|
||||
client.OrderParameters = false;
|
||||
|
||||
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null;
|
||||
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null;
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
|
||||
@@ -131,9 +131,9 @@ namespace CryptoExchange.Net.Testing
|
||||
client,
|
||||
new Uri(host.AppendPath(path)),
|
||||
method,
|
||||
uriParams,
|
||||
bodyParams,
|
||||
headers,
|
||||
ref uriParams,
|
||||
ref bodyParams,
|
||||
ref headers,
|
||||
true,
|
||||
client.ArraySerialization,
|
||||
client.ParameterPositions[method],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# CryptoExchange.Net
|
||||
#  CryptoExchange.Net
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) 
|
||||
|
||||
@@ -16,6 +16,7 @@ The following API's are directly supported. Note that there are 3rd party implem
|
||||
|BingX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[](https://www.nuget.org/packages/JK.BingX.Net)|
|
||||
|Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[](https://www.nuget.org/packages/Bitfinex.Net)|
|
||||
|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[](https://www.nuget.org/packages/JK.Bitget.Net)|
|
||||
|BitMart|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[](https://www.nuget.org/packages/BitMart.Net)|
|
||||
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|
|
||||
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|
|
||||
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|
|
||||
@@ -46,6 +47,59 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 7.11.1 - 25 Aug 2024
|
||||
* Improved closing logic websockets
|
||||
|
||||
* Version 7.11.0 - 07 Aug 2024
|
||||
* Added ParseString static method on EnumConverter for parsing strings manually
|
||||
* Added support for decimal values in System.Text.Json NumberStringConverter
|
||||
* Added support for `null` string values in System.Text.Json DecimalConverter
|
||||
* Added support for number deserialization when requesting string in System.Text.Json MessageAccessor.GetValue
|
||||
* Added deserialization handling of json values too big to fit decimal value
|
||||
* Decreased some memory allocations during rest request authentication
|
||||
* Fixed subscriptions trying to send unsubscribe request when the socket connection will be closed anyway
|
||||
* Removed SecureString usage in credentials; it's not recommended to be used
|
||||
* Removed some extension methods no longer relevant
|
||||
* Improved testing checks
|
||||
|
||||
* Version 7.10.0 - 26 Jul 2024
|
||||
* Added System.Text.Json NumberStringConverter
|
||||
* Added integration testing base class
|
||||
* Added AddSecondsString and AddOptionalSecondsString to ParameterCollection
|
||||
* Added Decompress method for ReadOnlyMemory using non-GZip deflate
|
||||
* Added SocketConnection parameter to SocketConnection PreprocessStreamMessage
|
||||
* Fixed websocket reconnect/unsubscribe timing bug
|
||||
* Fixed issue in System.Text.Json array object deserialization skipping property when skipping an index
|
||||
* Fixed order book logging bug
|
||||
* Fixed bug in ParameterCollection AddEnumAsInt
|
||||
|
||||
* Version 7.9.0 - 16 Jul 2024
|
||||
* Added some checks in websocket connection handling
|
||||
* Added As<T> and AsError<T> methods on untyped WebCallResult
|
||||
* Updated System.Text.Json package to version 8.0.4 to fix vulnerability
|
||||
* Updated websocket subscription response handling to remove the thread blocking ManualResetEvent usage
|
||||
* Updated static logging classes access modifier from internal to public so they can be called in overriden methods
|
||||
* Updated some testing object implementations
|
||||
* Fixed authentication error when reconnecting an unauthenticated connection which was marked as dedicated query connection
|
||||
* Small improvements in SystemTextJsonMessageAccessor
|
||||
* Fixed System.Text.Json ArrayConverter implementation nullable value types handling
|
||||
|
||||
* Version 7.8.0 - 02 Jul 2024
|
||||
* Updated single endpoint limit configuration
|
||||
* Added LongConverter for nullable longs
|
||||
* Updated SystemTextJsonComparer logic
|
||||
|
||||
* Version 7.7.3 - 26 Jun 2024
|
||||
* Fixed request ids not matching in logging
|
||||
* Added nullable int converter for System.Text.Json
|
||||
* Small fixes in tests
|
||||
|
||||
* Version 7.7.2 - 25 Jun 2024
|
||||
* Fixed ratelimiting issue possibly creating negative delays
|
||||
|
||||
* Version 7.7.1 - 23 Jun 2024
|
||||
* Fixes for caching implementation
|
||||
|
||||
* Version 7.7.0 - 23 Jun 2024
|
||||
* Caching support
|
||||
* Caching is supported for GET requests within a certain time frame
|
||||
|
||||
+56
-3
@@ -100,6 +100,7 @@
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_orderbooks">Orderbooks</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_logging">Logging</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_ratelimiting">Ratelimiting</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_caching">Caching</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_examples">Examples</a>
|
||||
@@ -140,6 +141,7 @@
|
||||
<tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr>
|
||||
<tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr>
|
||||
<tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr>
|
||||
<tr><td>BitMart</td><td><a href="https://github.com/JKorf/BitMart.Net">JKorf/BitMart.Net</a></td><td><a href="https://www.nuget.org/packages/BitMart.Net" target="_blank"><img src="https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square" /></a></td></tr>
|
||||
<tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr>
|
||||
<tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr>
|
||||
<tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr>
|
||||
@@ -1388,7 +1390,7 @@ await client.UnsubscribeAllAsync();</code></pre>
|
||||
============================ -->
|
||||
<section id="idocs_common">
|
||||
<h2>Common Clients</h2>
|
||||
<p>The CryptoClients.Net client exposes some common client classes. These clients aim to make using the different API's easier.</p>
|
||||
<p>The CryptoClients.Net library exposes two client classes. These clients aim to make using the different API's easier.</p>
|
||||
|
||||
<p><b>(I)ExchangeRestClient</b><br />
|
||||
The <code>IExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.
|
||||
@@ -1956,6 +1958,16 @@ var client = new OKXRestClient();</code></pre>
|
||||
<td>The interval of how often the time synchronization between client and server should be executed</td>
|
||||
<td><code>TimeSpan.FromHours(1)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CachingEnabled</td>
|
||||
<td>Whether or not client side caching should be enabled for GET requests, see <a href="#idocs_caching">Caching</a></td>
|
||||
<td><code>false</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CachingMaxAge</td>
|
||||
<td>The max age of data to return from the cache. If the same data is requested and the data is available in the client side cache and not older than this value the cached value is returned, else a new request will be done</td>
|
||||
<td><code>TimeSpan.FromSeconds(5)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>[API].ApiCredentials</td>
|
||||
<td>Same as the in the base options, allows overriding per sub-API</td>
|
||||
@@ -2060,7 +2072,10 @@ var client = new OKXRestClient();</code></pre>
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="book" role="tablist" style="margin-bottom: -16px;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="book-binance-tab" data-toggle="tab" href="#book-binance" role="tab" aria-controls="book-binance" aria-selected="true">Binance</a>
|
||||
<a class="nav-link active" id="book-cryptoclients-tab" data-toggle="tab" href="#book-cryptoclients" role="tab" aria-controls="book-cryptoclients" aria-selected="true">CryptoClients</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="book-binance-tab" data-toggle="tab" href="#book-binance" role="tab" aria-controls="book-binance" aria-selected="false">Binance</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="book-bingx-tab" data-toggle="tab" href="#book-bingx" role="tab" aria-controls="book-bingx" aria-selected="false">BingX</a>
|
||||
@@ -2094,7 +2109,21 @@ var client = new OKXRestClient();</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content my-3" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="book-binance" role="tabpanel" aria-labelledby="book-binance-tab">
|
||||
<div class="tab-pane fade show active" id="book-cryptoclients" role="tabpanel" aria-labelledby="book-cryptoclients-tab">
|
||||
<pre><code>// Assuming IExchangeOrderBookFactory is injected as bookFactoryClient
|
||||
var book = bookFactoryClient.Binance.Spot.Create("ETH", "USDT");
|
||||
var startResult = await book.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Book has successfully started and synchronized
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await book.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="book-binance" role="tabpanel" aria-labelledby="book-binance-tab">
|
||||
<pre><code>var book = new BinanceSpotSymbolOrderBook("ETHUSDT");
|
||||
var startResult = await book.StartAsync();
|
||||
if (!startResult.Success)
|
||||
@@ -2474,6 +2503,30 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="idocs_caching">
|
||||
<h2>Caching</h2>
|
||||
<p>
|
||||
Every REST API client based on the CryptoExchange.Net base library automatically supports caching of GET HTTP requests. A few advantages of caching:
|
||||
<ol>
|
||||
<li>Performance improvement, data response will be much faster as no roundtrip to the server is needed</li>
|
||||
<li>Reduced resource usage, returning data from the cache uses less resources than reading the server response, though there is some memory overhead</li>
|
||||
<li>Prevent rate limiting, the cache can be queried as many times as you like without having to worry about getting rate limited by the server</li>
|
||||
</ol>
|
||||
|
||||
<div class="alert alert-info">Caching is only applied for successful GET requests as GET requests by definition should not change state. Other HTTP method (POST, DELETE, etc) generally do change state, so caching those call would prevent an action being executed.</div>
|
||||
|
||||
</p>
|
||||
<p>
|
||||
To enable caching for GET requests set <code>CachingEnabled</code> to <code>true</code> in the client options. Optionally set the <code>CachingMaxAge</code> option to the desired value (default is 5 seconds).
|
||||
</p>
|
||||
<p>
|
||||
To determine whether a request has gotten the data from the server or from the local cache the <code>DataSource</code> property on the call result can inspected:
|
||||
<pre><code>var result = await bitfinexRestClient.SpotApi.Account.Get30DaySummaryAndFeesAsync();
|
||||
var responseSource = result.DataSource;</code></pre>
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
Reference in New Issue
Block a user