1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
JKorf cc1f0796fe Updated to version 7.11.2 2024-08-28 19:17:06 +02:00
JKorf b1cd9b5412 Fixed exception being thrown when waiting was canceled during rate limiting 2024-08-28 19:10:08 +02:00
Jonnern 42003a0247 Fix issue where SemaphoreSlim is released twice in RateLimitGate (#210) 2024-08-28 12:25:09 +02:00
JKorf d89c2bde94 Updated to version 7.11.1 2024-08-25 18:41:33 +02:00
JKorf 3e6bdaafc6 Improved closing logic websockets 2024-08-25 18:38:37 +02:00
JKorf 93e4722a81 Added testing checks for JsonInclude attribute for internal properties 2024-08-08 09:20:26 +02:00
JKorf 355ecb03da Updated to version 7.11.0 2024-08-07 18:42:00 +02:00
JKorf 994c527c1d Fixed warning 2024-08-07 18:39:01 +02:00
JKorf 69b2e2045e Fixed System.Text.Json tests not correctly checking capitalization 2024-08-07 16:49:12 +02:00
JKorf 7fde8bf5da Added converters/handling for values too big to fit decimal 2024-08-07 14:00:50 +02:00
JKorf 637070a7ae Fixed some warnings, added support for number deserialization when requesting string in STJ MessageAccessor.GetValue<T> 2024-08-06 15:37:15 +02:00
JKorf 7be75f72a7 Add check for null string to decimal converter 2024-08-05 21:45:01 +02:00
JKorf e3fece41f3 Small test fixes 2024-08-05 16:13:08 +02:00
JKorf 87b0c8d7a2 Docs 2024-08-02 13:28:42 +02:00
JKorf ca9a711f22 Support too large numbers for long value in NumberStringConverter, fall back to string 2024-08-02 12:07:15 +02:00
JKorf 27597bc994 Fix test 2024-08-02 08:59:14 +02:00
JKorf 776d75170d Fixed websocket client trying to unsubscribe subscription when the connection will be closed anyway 2024-08-02 08:59:04 +02:00
JKorf 949780a9ad Removed SecureString usage throughout the library, removed some object allocations, removed some unused extension methods 2024-08-01 22:43:06 +02:00
JKorf 2f64cd9f05 Add BitMart reference 2024-07-30 22:14:25 +02:00
JKorf 185dfeb6fb Added ParseString to EnumConverter for manual calling 2024-07-28 22:39:38 +02:00
40 changed files with 536 additions and 354 deletions
+6 -6
View File
@@ -51,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
// assert // assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10)); Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key.GetString() == "123"); Assert.That(options.ApiCredentials.Key == "123");
Assert.That(options.ApiCredentials.Secret.GetString() == "456"); Assert.That(options.ApiCredentials.Secret == "456");
} }
[Test] [Test]
@@ -64,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101"); options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert // assert
Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123"); Assert.That(options.Api1Options.ApiCredentials.Key == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456"); Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789"); Assert.That(options.Api2Options.ApiCredentials.Key == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101"); Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
} }
[Test] [Test]
+31 -16
View File
@@ -176,12 +176,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) 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); Assert.That(i == requests? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); 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); Assert.That(!triggered);
} }
@@ -201,7 +201,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) 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; bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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); 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); Assert.That(expectLimiting ? evnt != null : evnt == null);
} }
@@ -243,12 +243,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) 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); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); 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); Assert.That(!triggered);
} }
@@ -266,7 +266,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) 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; bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) 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; bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -309,9 +309,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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); 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); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -328,7 +328,7 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default); 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); Assert.That(expectLimited ? evnt != null : evnt == null);
@@ -348,9 +348,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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); 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); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -365,10 +365,25 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; 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); 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); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
[Test]
public async Task ConnectionRateLimiterCancel()
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
} }
} }
@@ -163,6 +163,20 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(output.Value == expected); 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("1", true)]
[TestCase("true", true)] [TestCase("true", true)]
[TestCase("yes", true)] [TestCase("yes", true)]
@@ -200,6 +214,41 @@ namespace CryptoExchange.Net.UnitTests
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}"); var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected); 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 public class STJTimeObject
@@ -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 GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret.GetString(); public string GetSecret() => _credentials.Secret;
} }
} }
@@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Security;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Converters.MessageParsing;
@@ -9,48 +8,23 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Api credentials, used to sign requests accessing private endpoints /// Api credentials, used to sign requests accessing private endpoints
/// </summary> /// </summary>
public class ApiCredentials: IDisposable public class ApiCredentials
{ {
/// <summary> /// <summary>
/// The api key to authenticate requests /// The api key to authenticate requests
/// </summary> /// </summary>
public SecureString? Key { get; } public string Key { get; }
/// <summary> /// <summary>
/// The api secret to authenticate requests /// The api secret to authenticate requests
/// </summary> /// </summary>
public SecureString? Secret { get; } public string Secret { get; }
/// <summary> /// <summary>
/// Type of the credentials /// Type of the credentials
/// </summary> /// </summary>
public ApiCredentialsType CredentialType { get; } 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> /// <summary>
/// Create Api credentials providing an api key and secret for authentication /// Create Api credentials providing an api key and secret for authentication
/// </summary> /// </summary>
@@ -72,8 +46,8 @@ namespace CryptoExchange.Net.Authentication
throw new ArgumentException("Key and secret can't be null/empty"); throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType; CredentialType = credentialsType;
Key = key.ToSecureString(); Key = key;
Secret = secret.ToSecureString(); Secret = secret;
} }
/// <summary> /// <summary>
@@ -82,8 +56,7 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
public virtual ApiCredentials Copy() public virtual ApiCredentials Copy()
{ {
// Use .GetString() to create a copy of the SecureString return new ApiCredentials(Key, Secret, CredentialType);
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
} }
/// <summary> /// <summary>
@@ -103,19 +76,10 @@ namespace CryptoExchange.Net.Authentication
if (key == null || secret == null) if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key.ToSecureString(); Key = key;
Secret = secret.ToSecureString(); Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin); inputStream.Seek(0, SeekOrigin.Begin);
} }
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Secret?.Dispose();
}
} }
} }
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Base class for authentication providers /// Base class for authentication providers
/// </summary> /// </summary>
public abstract class AuthenticationProvider : IDisposable public abstract class AuthenticationProvider
{ {
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Authentication
/// </summary> /// </summary>
protected byte[] _sBytes; protected byte[] _sBytes;
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -38,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
throw new ArgumentException("ApiKey/Secret needed"); throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials; _credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString()); _sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
} }
/// <summary> /// <summary>
@@ -58,9 +63,9 @@ namespace CryptoExchange.Net.Authentication
RestApiClient apiClient, RestApiClient apiClient,
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
IDictionary<string, object> uriParameters, ref IDictionary<string, object>? uriParameters,
IDictionary<string, object> bodyParameters, ref IDictionary<string, object>? bodyParameters,
Dictionary<string, string> headers, ref Dictionary<string, string>? headers,
bool auth, bool auth,
ArrayParametersSerialization arraySerialization, ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition, HttpMethodParameterPosition parameterPosition,
@@ -366,7 +371,7 @@ namespace CryptoExchange.Net.Authentication
{ {
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER
// Read from pem private key // Read from pem private key
var key = _credentials.Secret!.GetString() var key = _credentials.Secret!
.Replace("\n", "") .Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "") .Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "") .Replace("-----END PRIVATE KEY-----", "")
@@ -381,7 +386,7 @@ namespace CryptoExchange.Net.Authentication
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml) else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
{ {
// Read from xml private key format // Read from xml private key format
rsa.FromXmlString(_credentials.Secret!.GetString()); rsa.FromXmlString(_credentials.Secret!);
} }
else else
{ {
@@ -447,12 +452,6 @@ namespace CryptoExchange.Net.Authentication
else else
return serializer.Serialize(parameters); return serializer.Serialize(parameters);
} }
/// <inheritdoc />
public void Dispose()
{
_credentials?.Dispose();
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -65,10 +65,7 @@ namespace CryptoExchange.Net.Clients
BaseAddress = baseAddress; BaseAddress = baseAddress;
if (apiCredentials != null) if (apiCredentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
}
} }
/// <summary> /// <summary>
@@ -85,10 +82,7 @@ namespace CryptoExchange.Net.Clients
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{ {
if (credentials != null) if (credentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
}
} }
/// <summary> /// <summary>
@@ -97,7 +91,6 @@ namespace CryptoExchange.Net.Clients
public virtual void Dispose() public virtual void Dispose()
{ {
_disposing = true; _disposing = true;
AuthenticationProvider?.Dispose();
} }
} }
} }
+35 -36
View File
@@ -196,19 +196,20 @@ namespace CryptoExchange.Net.Clients
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null) where T : class
{ {
var key = baseAddress + definition + uriParameters?.ToFormData(); string? cacheKey = null;
if (ShouldCache(definition)) if (ShouldCache(definition))
{ {
_logger.CheckingCache(key); cacheKey = baseAddress + definition + uriParameters?.ToFormData();
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge); _logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null) if (cachedValue != null)
{ {
_logger.CacheHit(key); _logger.CacheHit(cacheKey);
var original = (WebCallResult<T>)cachedValue; var original = (WebCallResult<T>)cachedValue;
return original.Cached(); return original.Cached();
} }
_logger.CacheNotHit(key); _logger.CacheNotHit(cacheKey);
} }
int currentTry = 0; int currentTry = 0;
@@ -242,7 +243,7 @@ namespace CryptoExchange.Net.Clients
if (result.Success && if (result.Success &&
ShouldCache(definition)) ShouldCache(definition))
{ {
_cache.Add(key, result); _cache.Add(cacheKey!, result);
} }
return result; return result;
@@ -343,15 +344,15 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders) Dictionary<string, string>? additionalHeaders)
{ {
var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters); var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters); var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
var uri = new Uri(baseAddress.AppendPath(definition.Path)); var uri = new Uri(baseAddress.AppendPath(definition.Path));
var arraySerialization = definition.ArraySerialization ?? ArraySerialization; var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat; var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var headers = new Dictionary<string, string>(); Dictionary<string, string>? headers = null;
if (AuthenticationProvider != null) if (AuthenticationProvider != null)
{ {
try try
@@ -360,9 +361,9 @@ namespace CryptoExchange.Net.Clients
this, this,
uri, uri,
definition.Method, definition.Method,
uriParams, ref uriParams,
bodyParams, ref bodyParams,
headers, ref headers,
definition.Authenticated, definition.Authenticated,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
@@ -375,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 // 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); if (uriParams != null)
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId); var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
foreach (var header in headers) if (headers != null)
request.AddHeader(header.Key, header.Value); {
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null) if (additionalHeaders != null)
{ {
@@ -403,7 +408,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody) if (parameterPosition == HttpMethodParameterPosition.InBody)
{ {
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParams.Count != 0) if (bodyParams != null && bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType); WriteParamBody(request, bodyParams, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, contentType);
@@ -807,8 +812,8 @@ namespace CryptoExchange.Net.Clients
} }
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>(); var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null;
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>(); var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null;
if (AuthenticationProvider != null) if (AuthenticationProvider != null)
{ {
try try
@@ -817,9 +822,9 @@ namespace CryptoExchange.Net.Clients
this, this,
uri, uri,
method, method,
uriParameters, ref uriParameters,
bodyParameters, ref bodyParameters,
headers, ref headers,
signed, signed,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
@@ -832,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 // 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); var request = RequestFactory.Create(method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
foreach (var header in headers) if (headers != null)
request.AddHeader(header.Key, header.Value); {
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null) if (additionalHeaders != null)
{ {
@@ -870,7 +869,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody) if (parameterPosition == HttpMethodParameterPosition.InBody)
{ {
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Any()) if (bodyParameters?.Any() == true)
WriteParamBody(request, bodyParameters, contentType); WriteParamBody(request, bodyParameters, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, contentType);
@@ -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);
}
}
}
@@ -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) if (reader.TokenType == JsonTokenType.String)
{ {
var value = reader.GetString(); var value = reader.GetString();
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value) || string.Equals("null", value))
return null; 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 /> /// <inheritdoc />
@@ -211,5 +211,37 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); 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;
}
}
} }
} }
@@ -17,7 +17,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return null; return null;
if (reader.TokenType == JsonTokenType.Number) if (reader.TokenType == JsonTokenType.Number)
return reader.GetInt64().ToString(); {
if (reader.TryGetInt64(out var value))
return value.ToString();
return reader.GetDecimal().ToString();
}
return reader.GetString(); return reader.GetString();
} }
@@ -123,6 +123,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array) if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
return default; 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>(); return value.Value.Deserialize<T>();
} }
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <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> <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.10.0</PackageVersion> <PackageVersion>7.11.2</PackageVersion>
<AssemblyVersion>7.10.0</AssemblyVersion> <AssemblyVersion>7.11.2</AssemblyVersion>
<FileVersion>7.10.0</FileVersion> <FileVersion>7.11.2</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <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> <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> <RepositoryType>git</RepositoryType>
+3 -17
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System; using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -15,10 +16,6 @@ namespace CryptoExchange.Net
/// The last used id, use NextId() to get the next id and up this /// The last used id, use NextId() to get the next id and up this
/// </summary> /// </summary>
private static int _lastId; private static int _lastId;
/// <summary>
/// Lock for id generating
/// </summary>
private static object _idLock = new();
/// <summary> /// <summary>
/// Clamp a value between a min and max /// 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 /// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static int NextId() public static int NextId() => Interlocked.Increment(ref _lastId);
{
lock (_idLock)
{
_lastId += 1;
return _lastId;
}
}
/// <summary> /// <summary>
/// Return the last unique id that was generated /// Return the last unique id that was generated
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static int LastId() public static int LastId() => _lastId;
{
lock (_idLock)
return _lastId;
}
/// <summary> /// <summary>
/// Generate a random string of specified length /// Generate a random string of specified length
-106
View File
@@ -113,92 +113,6 @@ namespace CryptoExchange.Net
return formData.ToString(); 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> /// <summary>
/// Validates an int is one of the allowed values /// Validates an int is one of the allowed values
/// </summary> /// </summary>
@@ -318,26 +232,6 @@ namespace CryptoExchange.Net
return url.TrimEnd('/'); 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> /// <summary>
/// Create a new uri with the provided parameters as query /// Create a new uri with the provided parameters as query
/// </summary> /// </summary>
@@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestWeight">The weight of the request</param> /// <param name="requestWeight">The weight of the request</param>
/// <param name="ct">Cancellation token to cancel waiting</param> /// <param name="ct">Cancellation token to cancel waiting</param>
/// <returns>The time in milliseconds spend waiting</returns> /// <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);
} }
} }
@@ -25,6 +25,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException; private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished; 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?> _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?> _receivedPartialMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage; private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage; private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
@@ -33,6 +34,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished; 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?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect; private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
static CryptoExchangeWebSocketClientLoggingExtension() static CryptoExchangeWebSocketClientLoggingExtension()
{ {
@@ -170,6 +172,17 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug, LogLevel.Debug,
new EventId(1027, "NoDataReceiveTimeoutReconnect"), new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket"); "[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( public static void SocketConnecting(
@@ -286,6 +299,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null); _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( public static void SocketReceivedPartialMessage(
this ILogger logger, int socketId, int countBytes) this ILogger logger, int socketId, int countBytes)
{ {
@@ -333,5 +352,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null); _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 -14
View File
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// The password of the proxy /// The password of the proxy
/// </summary> /// </summary>
public SecureString? Password { get; } public string? Password { get; }
/// <summary> /// <summary>
/// Create new settings for a proxy /// Create new settings for a proxy
/// </summary> /// </summary>
/// <param name="host">The proxy hostname/ip</param> /// <param name="host">The proxy hostname/ip</param>
/// <param name="port">The proxy port</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="port">The proxy port</param>
/// <param name="login">The proxy login</param> /// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param> /// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, string? password) : this(host, port, login, password?.ToSecureString()) public ApiProxy(string host, int port, string? login, string? password)
{
}
/// <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)
{ {
Host = host; Host = host;
Port = port; Port = port;
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <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; => definition.Authenticated == _authenticated;
} }
} }
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <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); => string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
} }
} }
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <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); => _paths.Contains(definition.Path);
} }
} }
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <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; => host == _host;
} }
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <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; => type == _type;
} }
} }
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <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); => definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
} }
} }
@@ -14,26 +14,26 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Apply guard per host /// Apply guard per host
/// </summary> /// </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> /// <summary>
/// Apply guard per endpoint /// Apply guard per endpoint
/// </summary> /// </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> /// <summary>
/// Apply guard per API key /// Apply guard per API key
/// </summary> /// </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> /// <summary>
/// Apply guard per API key per endpoint /// Apply guard per API key per endpoint
/// </summary> /// </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 IEnumerable<IGuardFilter> _filters;
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private RateLimitWindowType _windowType; private RateLimitWindowType _windowType;
private double? _decayRate; private double? _decayRate;
private int? _connectionWeight; private int? _connectionWeight;
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector; private readonly Func<RequestDefinition, string, string?, string> _keySelector;
/// <inheritdoc /> /// <inheritdoc />
public string Name => "RateLimitGuard"; public string Name => "RateLimitGuard";
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</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) : 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="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</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; _filters = filters;
_trackers = new Dictionary<string, IWindowTracker>(); _trackers = new Dictionary<string, IWindowTracker>();
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <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) foreach(var filter in _filters)
{ {
@@ -114,7 +114,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <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) foreach (var filter in _filters)
{ {
@@ -38,7 +38,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <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; var dif = (After + _windowBuffer) - DateTime.UtcNow;
if (dif <= TimeSpan.Zero) if (dif <= TimeSpan.Zero)
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <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; return RateLimitState.NotApplied;
} }
@@ -15,19 +15,19 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Default endpoint limit /// Default endpoint limit
/// </summary> /// </summary>
public static Func<RequestDefinition, string, SecureString?, string> Default { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method); public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
/// <summary> /// <summary>
/// Endpoint limit per API key /// Endpoint limit per API key
/// </summary> /// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method); 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 Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate; private readonly double? _decayRate;
private readonly int _limit; private readonly int _limit;
private readonly TimeSpan _period; private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector; private readonly Func<RequestDefinition, string, string?, string> _keySelector;
/// <inheritdoc /> /// <inheritdoc />
public string Name => "EndpointLimitGuard"; public string Name => "EndpointLimitGuard";
@@ -43,7 +43,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
TimeSpan period, TimeSpan period,
RateLimitWindowType windowType, RateLimitWindowType windowType,
double? decayRate = null, double? decayRate = null,
Func<RequestDefinition, string, SecureString?, string>? keySelector = null) Func<RequestDefinition, string, string?, string>? keySelector = null)
{ {
_limit = limit; _limit = limit;
_period = period; _period = period;
@@ -54,7 +54,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <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 = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey);
if (!_trackers.TryGetValue(key, out var tracker)) if (!_trackers.TryGetValue(key, out var tracker))
@@ -71,7 +71,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <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 = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey);
var tracker = _trackers[key]; var tracker = _trackers[key];
@@ -16,6 +16,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="host">The host address</param> /// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <returns>True if passed</returns> /// <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);
} }
} }
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <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> /// <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 /// 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
@@ -66,6 +66,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, 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="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <returns></returns> /// <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> /// <summary>
/// Apply the request to this guard with the specified weight /// 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="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <returns></returns> /// <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);
} }
} }
@@ -32,22 +32,30 @@ namespace CryptoExchange.Net.RateLimiting
{ {
_name = name; _name = name;
_guards = new ConcurrentBag<IRateLimitGuard>(); _guards = new ConcurrentBag<IRateLimitGuard>();
_semaphore = new SemaphoreSlim(1); _semaphore = new SemaphoreSlim(1, 1);
} }
/// <inheritdoc /> /// <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); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally finally
{ {
_waitingCount--; _waitingCount--;
_semaphore.Release(); if (release)
_semaphore.Release();
} }
} }
@@ -58,26 +66,33 @@ namespace CryptoExchange.Net.RateLimiting
IRateLimitGuard guard, IRateLimitGuard guard,
RateLimitItemType type, RateLimitItemType type,
RequestDefinition definition, RequestDefinition definition,
string host, string host,
SecureString? apiKey, string? apiKey,
RateLimitingBehaviour rateLimitingBehaviour, RateLimitingBehaviour rateLimitingBehaviour,
CancellationToken ct) CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally finally
{ {
_waitingCount--; _waitingCount--;
_semaphore.Release(); if (release)
_semaphore.Release();
} }
} }
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) foreach (var guard in guards)
{ {
@@ -233,14 +233,14 @@ namespace CryptoExchange.Net.Sockets
while (!_stopRequested) while (!_stopRequested)
{ {
_logger.SocketStartingProcessing(Id); _logger.SocketStartingProcessing(Id);
_processState = ProcessState.Processing; SetProcessState(ProcessState.Processing);
var sendTask = SendLoopAsync(); var sendTask = SendLoopAsync();
var receiveTask = ReceiveLoopAsync(); var receiveTask = ReceiveLoopAsync();
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask; var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false); await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
_logger.SocketFinishedProcessing(Id); _logger.SocketFinishedProcessing(Id);
_processState = ProcessState.WaitingForClose; SetProcessState(ProcessState.WaitingForClose);
while (_closeTask == null) while (_closeTask == null)
await Task.Delay(50).ConfigureAwait(false); await Task.Delay(50).ConfigureAwait(false);
@@ -250,14 +250,14 @@ namespace CryptoExchange.Net.Sockets
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled) if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
{ {
_processState = ProcessState.Idle; SetProcessState(ProcessState.Idle);
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return; return;
} }
if (!_stopRequested) if (!_stopRequested)
{ {
_processState = ProcessState.Reconnecting; SetProcessState(ProcessState.Reconnecting);
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
} }
@@ -296,12 +296,15 @@ namespace CryptoExchange.Net.Sockets
_reconnectAttempt = 0; _reconnectAttempt = 0;
_lastReconnectTime = DateTime.UtcNow; _lastReconnectTime = DateTime.UtcNow;
// Set to processing before reconnect handling
SetProcessState(ProcessState.Processing);
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
break; break;
} }
} }
_processState = ProcessState.Idle; SetProcessState(ProcessState.Idle);
} }
private TimeSpan GetReconnectDelay() private TimeSpan GetReconnectDelay()
@@ -391,34 +394,33 @@ namespace CryptoExchange.Net.Sockets
if (_disposed) if (_disposed)
return; return;
_ctsSource.Cancel(); try
if (_socket.State == WebSocketState.Open)
{ {
try if (_socket.State == WebSocketState.CloseReceived)
{
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
{ {
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false); 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 await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync var startWait = DateTime.UtcNow;
// So socket might go to aborted state, might still be open 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> /// <summary>
@@ -565,10 +567,20 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult.MessageType == WebSocketMessageType.Close) if (receiveResult.MessageType == WebSocketMessageType.Close)
{ {
// Connection closed unexpectedly // Connection closed
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); if (_socket.State == WebSocketState.CloseReceived)
if (_closeTask?.IsCompleted != false) {
_closeTask = CloseInternalAsync(); // 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; break;
} }
@@ -758,6 +770,15 @@ namespace CryptoExchange.Net.Sockets
if (proxy.Login != null) if (proxy.Login != null)
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); 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> /// <summary>
@@ -590,12 +590,16 @@ namespace CryptoExchange.Net.Sockets
bool anyDuplicateSubscription; bool anyDuplicateSubscription;
lock (_listenersLock) lock (_listenersLock)
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l))); 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) if (!anyDuplicateSubscription)
{ {
bool needUnsub; bool needUnsub;
lock (_listenersLock) lock (_listenersLock)
needUnsub = _listeners.Contains(subscription); needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
if (needUnsub && _socket.IsOpen) if (needUnsub && _socket.IsOpen)
await UnsubscribeAsync(subscription).ConfigureAwait(false); await UnsubscribeAsync(subscription).ConfigureAwait(false);
@@ -611,16 +615,9 @@ namespace CryptoExchange.Net.Sockets
return; return;
} }
bool shouldCloseConnection;
lock (_listenersLock)
{
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
if (shouldCloseConnection)
Status = SocketStatus.Closing;
}
if (shouldCloseConnection) if (shouldCloseConnection)
{ {
Status = SocketStatus.Closing;
_logger.ClosingNoMoreSubscriptions(SocketId); _logger.ClosingNoMoreSubscriptions(SocketId);
await CloseAsync().ConfigureAwait(false); await CloseAsync().ConfigureAwait(false);
} }
@@ -151,17 +151,35 @@ namespace CryptoExchange.Net.Testing.Comparers
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties) 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 // Property has a value
var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p; 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 == prop.Name).p;
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
if (property is null) if (property is null)
// Property not found // Property not found
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`"); 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); var propertyValue = property.GetValue(obj);
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties); CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
} }
@@ -357,6 +375,14 @@ namespace CryptoExchange.Net.Testing.Comparers
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!)) if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}"); 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) else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
{ {
// TODO enum comparing // TODO enum comparing
@@ -9,12 +9,18 @@ namespace CryptoExchange.Net.Testing
{ {
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); 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) public override void WriteLine(string message)
{ {
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
if (message.Contains("Received null enum value"))
throw new Exception("Enum null error: " + message);
} }
} }
} }
+5 -5
View File
@@ -121,8 +121,8 @@ namespace CryptoExchange.Net.Testing
if (disableOrdering) if (disableOrdering)
client.OrderParameters = false; client.OrderParameters = false;
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? 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) : new Dictionary<string, object>(); var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null;
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
@@ -131,9 +131,9 @@ namespace CryptoExchange.Net.Testing
client, client,
new Uri(host.AppendPath(path)), new Uri(host.AppendPath(path)),
method, method,
uriParams, ref uriParams,
bodyParams, ref bodyParams,
headers, ref headers,
true, true,
client.ArraySerialization, client.ArraySerialization,
client.ParameterPositions[method], client.ParameterPositions[method],
+19
View File
@@ -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)|[![Nuget version](https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.BingX.Net)| |BingX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.BingX.Net)|
|Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)| |Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|
|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)| |Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|
|BitMart|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)| |Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)| |CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)| |CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|
@@ -46,6 +47,24 @@ 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). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 7.11.2 - 28 Aug 2024
* Fixed issues when ratelimiting is canceled using the provided cancellation token
* 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 * Version 7.10.0 - 26 Jul 2024
* Added System.Text.Json NumberStringConverter * Added System.Text.Json NumberStringConverter
* Added integration testing base class * Added integration testing base class
+20 -2
View File
@@ -141,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>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>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>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>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>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> <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>
@@ -2071,7 +2072,10 @@ var client = new OKXRestClient();</code></pre>
<div class="tab-wrap"> <div class="tab-wrap">
<ul class="nav nav-tabs" id="book" role="tablist" style="margin-bottom: -16px;"> <ul class="nav nav-tabs" id="book" role="tablist" style="margin-bottom: -16px;">
<li class="nav-item" role="presentation"> <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>
<li class="nav-item" role="presentation"> <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> <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>
@@ -2105,7 +2109,21 @@ var client = new OKXRestClient();</code></pre>
</li> </li>
</ul> </ul>
<div class="tab-content my-3" id="myTabContent"> <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"); <pre><code>var book = new BinanceSpotSymbolOrderBook("ETHUSDT");
var startResult = await book.StartAsync(); var startResult = await book.StartAsync();
if (!startResult.Success) if (!startResult.Success)