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

Compare commits

...

63 Commits

Author SHA1 Message Date
Jkorf 3d6267da93 Added specific logging for user cancellation on rest requests instead of generic warning log 2025-03-05 09:02:25 +01:00
JKorf 8def7f32af Update UnsubscribeAll in socket client to also unsubscribe on a dedicated connection 2025-03-04 19:45:06 +01:00
Jkorf ac295de9f6 Added referal link 2025-03-04 11:46:03 +01:00
Jkorf d412e0895e Added DeepCoin reference and examples 2025-03-04 11:41:03 +01:00
Jkorf 1f9e2b4fcb Fixed websocket ping timeout not recognized for warning logging 2025-02-26 10:44:57 +01:00
James Carter b13cff5a95 Fix memory leak in AsyncAutoResetEvent (#229)
* Fix memory leak in AsyncAutoResetEvent

CancellationTokenRegistration MUST be disposed, as the CancellationToken passed is saved for the lifetime of the Client, and registrations build up forever.
2025-02-24 08:33:26 +01:00
James Carter 4c050744ad Allow specifying the ReceiveMessageBuffer size on Websockets (#228)
In order to support more User websocket connections, allow reducing the memory requirements for the receive buffer, keeping the default buffer.
2025-02-23 19:58:05 +01:00
JKorf 3b15c35a02 Added support for ratelimiting key suffix, allowing parameter based ratelimiting 2025-02-17 17:26:04 +01:00
Jkorf cd78dbf575 Updated to version 8.8.0 2025-02-10 14:38:19 +01:00
Jkorf a258532d6a Fixed DataTime copying in DataEvent 2025-02-10 14:32:25 +01:00
JKorf d2a87a1069 Added additional enum values to default SupportIntervals for shared rest and socket kline operations 2025-02-09 21:43:18 +01:00
JKorf e07f24ea0a Fixed various info-warnings and spelling issues 2025-02-09 21:25:26 +01:00
JKorf 024e8dcfe2 Added SharedKlineInterval values 2025-02-09 20:12:55 +01:00
JKorf 4bb5aae40a Split DataEvent.Timestamp in DataEvent.ReceivedTimestamp and 2025-02-09 16:40:28 +01:00
JKorf dec94678ec Updated to version 8.7.4 2025-02-08 14:28:29 +01:00
JKorf 1a49fc8251 Fix exception when creating rest client for mono runtime 2025-02-08 14:25:19 +01:00
Jkorf 29b0875960 Updated examples 2025-02-07 13:50:00 +01:00
Jkorf 976ccab1da Added BitMEX reference 2025-02-07 13:20:54 +01:00
Jkorf 02bbd37bb6 Updated to version 8.7.3 2025-02-05 09:15:25 +01:00
Jkorf 1bbbec7f2b Fixed issue with serialization of nullable types in System.Text.Json ArrayConverter 2025-02-05 09:12:12 +01:00
Jkorf 0262f04913 Added handling of negative number DateTime deserialization to default 2025-02-05 08:25:59 +01:00
Jkorf fd1ec17d72 Fix for unnecessary error message in logging when closing connection 2025-02-04 08:28:48 +01:00
Jkorf 4bdad7fe0c Updated SharedSymbol from class to record 2025-02-04 08:28:21 +01:00
Jkorf 74f73dc790 Updated to version 8.7.2 2025-01-27 13:24:08 +01:00
Jkorf 0527a8a76e Some small fixes in the System.Text.Json ArrayConverter, added support for flags in EnumConverter 2025-01-27 11:52:07 +01:00
Jkorf c693eb8c02 Updated to version 8.7.1 2025-01-24 08:42:08 +01:00
Jkorf 3eb28c7fed Added HyperLiquid referral 2025-01-23 09:35:44 +01:00
JKorf 618c4922b9 Added Authenticated property to IBaseApiClient interface 2025-01-22 19:11:38 +01:00
Jkorf c81b15861d Updated examples and docs with HyperLiquid references 2025-01-21 15:24:23 +01:00
Jkorf 4a5832cccd Updated to version 8.7.0 2025-01-21 14:02:42 +01:00
Jkorf 4e47c4cbdf Updated CheckForMissingInterfaces test 2025-01-21 14:00:30 +01:00
Jkorf 2af1520ecc Added PriceSignificationFigures to SharedSpotSymbol model 2025-01-21 14:00:08 +01:00
Jkorf cf397af3ab Added GetMillisecondTimestampLong helper method to AuthenticationProvider 2025-01-21 13:47:05 +01:00
JKorf a1479705e2 Fixed typo 2025-01-13 17:49:38 +01:00
Jkorf 175e23f110 Updated to version 8.6.1 2025-01-09 16:25:11 +01:00
Jkorf 9b7019ded2 Removed websocket Error callback when exception is expected 2025-01-09 16:23:34 +01:00
Jkorf 7904aa9ba7 Fixed websocket connection getting stuck after a ping frame timeout, removed unnecessary type restraints on RestApiClient.SendAsync methods 2025-01-09 16:18:13 +01:00
Jkorf 3fe6db589f Updated to version 8.6.0 2025-01-07 13:25:50 +01:00
Jkorf 625dccbbe4 Added ExchangeType enum, some small improvements 2025-01-07 13:22:16 +01:00
Jkorf e650771d16 Added response headers parameter to RestApiClient.TryParseError method, added check for ServerRateLimitError on the result 2025-01-07 10:19:37 +01:00
Jkorf 3dad28b19d Added IFeeRestClient to service registration 2025-01-07 08:59:51 +01:00
Jkorf 2b9fda985e Add support for passing weight to apply to an individual ratelimit guard 2025-01-07 08:35:06 +01:00
JKorf ff8759409b Use Convert.ToHexString if available 2025-01-06 21:38:42 +01:00
JKorf 0d9627c13f Changed socket no data reconnect message to LogLevel Warning 2024-12-23 20:03:56 +01:00
Jkorf 0179fd7e2a Fixed workflow automated tests 2024-12-23 14:43:23 +01:00
Jkorf b8d0b0cf95 Workflow fix 2024-12-23 14:36:18 +01:00
Jkorf 73c42bd452 Updated to version 8.5.0 2024-12-23 14:25:39 +01:00
Jkorf 290be7f5e0 Added net9.0 build target, added KeepAliveTimeout for websocket connections 2024-12-23 14:14:47 +01:00
Jan Korf 0be1bb16e3 Feature/update settings (#225)
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
2024-12-23 08:49:58 +01:00
Jkorf 8605196390 Updated to version 8.4.5 2024-12-20 15:25:41 +01:00
Jkorf 460dd97537 Added EmptyArrayObjectConverter, added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor ctor 2024-12-20 15:21:21 +01:00
JKorf 1ec5984fad Updated to version 8.4.4 2024-12-08 10:16:19 +01:00
JKorf 8260c2661d Changed JsonConverterCtorAttribute to use type parameter instead of generic parameter to support .net framework 2024-12-08 10:12:39 +01:00
Jkorf 591c1dd405 docs 2024-12-04 10:44:35 +01:00
Jkorf 0164cdfcc4 docs 2024-12-04 09:15:23 +01:00
Jkorf 23a6cfff87 docs 2024-12-04 09:07:59 +01:00
Jkorf fdcdb90a5f Added XT reference 2024-12-04 08:58:46 +01:00
Jkorf 0b7107401f Updated to version 8.4.3 2024-12-03 09:59:36 +01:00
JKorf 06add65354 Fixed KlineTracker update handling 2024-12-02 21:46:30 +01:00
Jkorf 773d288497 Updated to version 8.4.2 2024-12-02 14:39:27 +01:00
Jkorf fd4e8da938 Removed special characters in ClientOrderIdSeperator to adhere to field content rules 2024-12-02 14:38:22 +01:00
Jkorf 271743b669 Updated to version 8.4.1 2024-12-02 13:15:31 +01:00
Jkorf f4797caf37 Added replace converter, added library helpers class 2024-12-02 13:13:56 +01:00
111 changed files with 1664 additions and 404 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 8.0.x dotnet-version: 9.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
@@ -106,6 +106,7 @@ namespace CryptoExchange.Net.UnitTests
for(var i = 1; i <= 10; i++) for(var i = 1; i <= 10; i++)
{ {
evnt.Set(); evnt.Set();
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion)); Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
} }
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
var result = ExchangeHelpers.Normalize(input); var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture)); Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
} }
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
} }
} }
+19 -19
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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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, 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -328,9 +328,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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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, null, 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", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -365,9 +365,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.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, 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", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -381,8 +381,8 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); 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 result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>()); Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
} }
} }
@@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
/// <summary> /// <summary>
/// Map a enum entry to string values /// Map a enum entry to string values
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class MapAttribute : Attribute public class MapAttribute : Attribute
{ {
/// <summary> /// <summary>
@@ -1,4 +1,4 @@
#if !NETSTANDARD2_1 #if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis namespace System.Diagnostics.CodeAnalysis
{ {
using System; using System;
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create(); var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem) if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{ {
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key // Read from pem private key
var key = _credentials.Secret! var key = _credentials.Secret!
.Replace("\n", "") .Replace("\n", "")
@@ -403,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static string BytesToHexString(byte[] buff) protected static string BytesToHexString(byte[] buff)
{ {
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty; var result = string.Empty;
foreach (var t in buff) foreach (var t in buff)
result += t.ToString("X2"); result += t.ToString("X2");
return result; return result;
#endif
} }
/// <summary> /// <summary>
@@ -439,16 +443,26 @@ namespace CryptoExchange.Net.Authentication
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
} }
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
}
/// <summary> /// <summary>
/// Return the serialized request body /// Return the serialized request body
/// </summary> /// </summary>
/// <param name="serializer"></param> /// <param name="serializer"></param>
/// <param name="parameters"></param> /// <param name="parameters"></param>
/// <returns></returns> /// <returns></returns>
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters) protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
{ {
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey)) if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]); return serializer.Serialize(value);
else else
return serializer.Serialize(parameters); return serializer.Serialize(parameters);
} }
+1 -1
View File
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns> /// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge) public object? Get(string key, TimeSpan maxAge)
{ {
_cache.TryGetValue(key, out CacheItem value); _cache.TryGetValue(key, out CacheItem? value);
if (value == null) if (value == null)
return null; return null;
+13 -4
View File
@@ -38,9 +38,7 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public bool OutputOriginalData { get; } public bool OutputOriginalData { get; }
/// <summary> /// <inheritdoc />
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null; public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
/// <summary> /// <summary>
@@ -57,7 +55,7 @@ namespace CryptoExchange.Net.Clients
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger">Logger</param> /// <param name="logger">Logger</param>
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param> /// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param> /// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param> /// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param> /// <param name="clientOptions">Client options</param>
@@ -93,6 +91,17 @@ namespace CryptoExchange.Net.Clients
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
} }
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+3 -3
View File
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Version of the CryptoExchange.Net base library /// Version of the CryptoExchange.Net base library
/// </summary> /// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version; public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary> /// <summary>
/// Version of the client implementation /// Version of the client implementation
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Clients
lock(_versionLock) lock(_versionLock)
{ {
if (_exchangeVersion == null) if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version; _exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion; return _exchangeVersion;
} }
@@ -49,7 +49,7 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected internal ILogger _logger; protected internal ILogger _logger;
private object _versionLock = new object(); private readonly object _versionLock = new object();
private Version _exchangeVersion; private Version _exchangeVersion;
/// <summary> /// <summary>
@@ -93,6 +93,7 @@ namespace CryptoExchange.Net.Clients
{ {
tasks.Add(client.ReconnectAsync()); tasks.Add(client.ReconnectAsync());
} }
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
} }
@@ -106,6 +107,7 @@ namespace CryptoExchange.Net.Clients
{ {
result.AppendLine(client.GetSubscriptionsState()); result.AppendLine(client.GetSubscriptionsState());
} }
return result.ToString(); return result.ToString();
} }
@@ -120,6 +122,7 @@ namespace CryptoExchange.Net.Clients
{ {
result.Add(client.GetState()); result.Add(client.GetState());
} }
return result; return result;
} }
} }
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public class CryptoBaseClient : IDisposable public class CryptoBaseClient : IDisposable
{ {
private Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>(); private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary> /// <summary>
/// Service provider /// Service provider
+63 -22
View File
@@ -89,7 +89,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Memory cache /// Memory cache
/// </summary> /// </summary>
private static MemoryCache _cache = new MemoryCache(); private readonly static MemoryCache _cache = new MemoryCache();
/// <summary> /// <summary>
/// ctor /// ctor
@@ -154,6 +154,8 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>( protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -161,7 +163,9 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? parameters, ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{ {
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>( return SendAsync<T>(
@@ -171,7 +175,9 @@ namespace CryptoExchange.Net.Clients
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken, cancellationToken,
additionalHeaders, additionalHeaders,
weight); weight,
weightSingleLimiter,
rateLimitKeySuffix);
} }
/// <summary> /// <summary>
@@ -185,6 +191,8 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>( protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -193,7 +201,9 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{ {
string? cacheKey = null; string? cacheKey = null;
if (ShouldCache(definition)) if (ShouldCache(definition))
@@ -217,7 +227,7 @@ namespace CryptoExchange.Net.Clients
currentTry++; currentTry++;
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false);
if (!prepareResult) if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!); return new WebCallResult<T>(prepareResult.Error!);
@@ -231,10 +241,17 @@ namespace CryptoExchange.Net.Clients
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++; TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (!result) if (result.Error is not CancellationRequestedError)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString()); {
if (!result)
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
}
else else
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"); {
_logger.RestApiCancellationRequested(result.RequestId);
}
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false)) if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
continue; continue;
@@ -258,6 +275,8 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param> /// <param name="weight">Override the request weight for this request</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync( protected virtual async Task<CallResult> PrepareAsync(
@@ -266,10 +285,10 @@ namespace CryptoExchange.Net.Clients
RequestDefinition definition, RequestDefinition definition,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{ {
var requestWeight = weight ?? definition.Weight;
// Time sync // Time sync
if (definition.Authenticated) if (definition.Authenticated)
{ {
@@ -295,6 +314,7 @@ namespace CryptoExchange.Net.Clients
} }
// Rate limiting // Rate limiting
var requestWeight = weight ?? definition.Weight;
if (requestWeight != 0) if (requestWeight != 0)
{ {
if (definition.RateLimitGate == null) if (definition.RateLimitGate == null)
@@ -302,7 +322,7 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult(limitResult.Error!); return new CallResult(limitResult.Error!);
} }
@@ -316,7 +336,8 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult(limitResult.Error!); return new CallResult(limitResult.Error!);
} }
@@ -602,7 +623,7 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false); var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, null, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult<IRequest>(limitResult.Error!); return new CallResult<IRequest>(limitResult.Error!);
} }
@@ -617,7 +638,7 @@ namespace CryptoExchange.Net.Clients
paramString = $" with request body '{request.Content}'"; paramString = $" with request body '{request.Content}'";
var headers = request.GetHeaders(); var headers = request.GetHeaders();
if (headers.Any()) if (headers.Count != 0)
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")); paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++; TotalRequestsMade++;
@@ -693,10 +714,21 @@ namespace CryptoExchange.Net.Clients
} }
// Json response received // Json response received
var parsedError = TryParseError(accessor); var parsedError = TryParseError(response.ResponseHeaders, accessor);
if (parsedError != null) if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
{
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
}
// Success status code, but TryParseError determined it was an error response // Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
@@ -730,12 +762,13 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not. /// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null /// If the response is an error this method should return the parsed error, else it should return null
/// </summary> /// </summary>
/// <param name="accessor">Data accessor</param> /// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns> /// <returns>Null if not an error, Error otherwise</returns>
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null; protected virtual Error? TryParseError(IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor) => null;
/// <summary> /// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@@ -752,7 +785,7 @@ namespace CryptoExchange.Net.Clients
// Only retry once // Only retry once
return false; return false;
if ((int?)callResult.ResponseStatusCode == 429 if (callResult.Error is ServerRateLimitError
&& ClientOptions.RateLimiterEnabled && ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null) && gate != null)
@@ -807,7 +840,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InUri) if (parameterPosition == HttpMethodParameterPosition.InUri)
{ {
foreach (var parameter in parameters) foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()); uri = uri.AddQueryParameter(parameter.Key, parameter.Value.ToString()!);
} }
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
@@ -889,8 +922,8 @@ namespace CryptoExchange.Net.Clients
{ {
// Write the parameters as json in the body // Write the parameters as json in the body
string stringData; string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey)) if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]); stringData = CreateSerializer().Serialize(value);
else else
stringData = CreateSerializer().Serialize(parameters); stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType); request.SetContent(stringData, contentType);
@@ -961,6 +994,14 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns> /// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
+33 -9
View File
@@ -82,7 +82,7 @@ namespace CryptoExchange.Net.Clients
{ {
get get
{ {
if (!socketConnections.Any()) if (socketConnections.IsEmpty)
return 0; return 0;
return socketConnections.Sum(s => s.Value.IncomingKbps); return socketConnections.Sum(s => s.Value.IncomingKbps);
@@ -97,7 +97,7 @@ namespace CryptoExchange.Net.Clients
{ {
get get
{ {
if (!socketConnections.Any()) if (socketConnections.IsEmpty)
return 0; return 0;
return socketConnections.Sum(s => s.Value.UserSubscriptionCount); return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
@@ -158,7 +158,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -422,9 +422,10 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message; result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!; return new CallResult(result.Error)!;
} }
_logger.Authenticated(socket.SocketId);
} }
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true; socket.Authenticated = true;
return new CallResult(null); return new CallResult(null);
} }
@@ -509,7 +510,7 @@ namespace CryptoExchange.Net.Clients
if (connection != null) if (connection != null)
{ {
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)) if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new // Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(connection); return new CallResult<SocketConnection>(connection);
} }
@@ -597,9 +598,10 @@ namespace CryptoExchange.Net.Clients
KeepAliveInterval = KeepAliveInterval, KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval, ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null, RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehaviour = ClientOptions.RateLimitingBehaviour, RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
Proxy = ClientOptions.Proxy, Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
}; };
/// <summary> /// <summary>
@@ -669,8 +671,11 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
var socketList = socketConnections.Values; var socketList = socketConnections.Values;
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)) foreach (var connection in socketList)
tasks.Add(connection.CloseAsync()); {
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
tasks.Add(connection.CloseAsync(subscription));
}
} }
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@@ -710,6 +715,25 @@ namespace CryptoExchange.Net.Clients
return new CallResult(null); return new CallResult(null);
} }
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
var result = Activator.CreateInstance(objectType); var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader); var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType); return ParseObject(arr, result!, objectType);
} }
private static object ParseObject(JArray arr, object result, Type objectType) private static object ParseObject(JArray arr, object result, Type objectType)
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
var count = 0; var count = 0;
if (innerArray.Count == 0) if (innerArray.Count == 0)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else if (innerArray[0].Type == JTokenType.Array) else if (innerArray[0].Type == JTokenType.Array)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
foreach (var obj in innerArray) foreach (var obj in innerArray)
{ {
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!); arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
count++; count++;
} }
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else else
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!); arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
continue; continue;
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
object? value; object? value;
if (converterAttribute != null) if (converterAttribute != null)
{ {
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
} }
else if (conversionAttribute != null) else if (conversionAttribute != null)
{ {
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
else if ((property.PropertyType == typeof(decimal) else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?)) || property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0)) && (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
var v = value.ToString(); var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec)) if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
last = arrayProp.Index; last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop); var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null) if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType))); writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
else if (!IsSimple(prop.PropertyType)) else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value)); serializer.Serialize(writer, prop.GetValue(value));
else else
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute => private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))); (T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
private static T? GetCustomAttribute<T>(Type type) where T : Attribute => private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))); (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
} }
} }
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
{ {
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
var value = reader.Value!.ToString(); var value = reader.Value!.ToString()!;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
/// </returns> /// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var value = reader.Value?.ToString().ToLower().Trim(); var value = reader.Value?.ToString()!.ToLower().Trim();
if (value == null || value == "") if (value == null || value == "")
{ {
if (Nullable.GetUnderlyingType(objectType) != null) if (Nullable.GetUnderlyingType(objectType) != null)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader); using var jsonTextReader = new JsonTextReader(reader);
@@ -3,7 +3,7 @@
/// <summary> /// <summary>
/// Node accessor /// Node accessor
/// </summary> /// </summary>
public struct NodeAccessor public readonly struct NodeAccessor
{ {
/// <summary> /// <summary>
/// Index /// Index
@@ -6,9 +6,9 @@ namespace CryptoExchange.Net.Converters.MessageParsing
/// <summary> /// <summary>
/// Message access definition /// Message access definition
/// </summary> /// </summary>
public struct MessagePath : IEnumerable<NodeAccessor> public readonly struct MessagePath : IEnumerable<NodeAccessor>
{ {
private List<NodeAccessor> _path; private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node) internal void Add(NodeAccessor node)
{ {
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class ArrayPropertyInfo private class ArrayPropertyInfo
@@ -79,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonSerializerOptions? typeOptions = null; JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null) if (prop.JsonConverterType != null)
{ {
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
typeOptions = new JsonSerializerOptions(); typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear(); typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter); typeOptions.Converters.Add(converter);
@@ -87,10 +87,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType)) if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
{ {
if (prop.PropertyInfo.PropertyType == typeof(string)) if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if(prop.TargetType.IsEnum)
writer.WriteStringValue(EnumConverter.GetString(objValue));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
} }
else else
{ {
@@ -107,7 +111,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
return default; return default;
var result = Activator.CreateInstance(typeToConvert); var result = Activator.CreateInstance(typeToConvert)!;
return (T)ParseObject(ref reader, result, typeToConvert, options); return (T)ParseObject(ref reader, result, typeToConvert, options);
} }
@@ -177,7 +181,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
{ {
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
newOptions = new JsonSerializerOptions newOptions = new JsonSerializerOptions
{ {
NumberHandling = SerializerOptions.WithConverters.NumberHandling, NumberHandling = SerializerOptions.WithConverters.NumberHandling,
@@ -187,12 +191,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions); _converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
} }
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, newOptions); value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
} }
else if (attribute.DefaultDeserialization) else if (attribute.DefaultDeserialization)
{ {
// Use default deserialization // Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options); value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters);
} }
else else
{ {
@@ -209,7 +213,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value == null ? null : value); attribute.PropertyInfo.SetValue(result, value);
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch(OverflowException) catch(OverflowException)
{ {
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class BoolConverterInner<T> : JsonConverter<T> private class BoolConverterInner<T> : JsonConverter<T>
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class DateTimeConverterInner<T> : JsonConverter<T> private class DateTimeConverterInner<T> : JsonConverter<T>
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number) if (reader.TokenType is JsonTokenType.Number)
{ {
var longValue = reader.GetDouble(); var longValue = reader.GetDouble();
if (longValue == 0 || longValue == -1) if (longValue == 0 || longValue < 0)
return default; return default;
return ParseFromDouble(longValue); return ParseFromDouble(longValue);
@@ -74,7 +74,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
{
writer.WriteNullValue(); writer.WriteNullValue();
}
else else
{ {
var dtValue = (DateTime)(object)value; var dtValue = (DateTime)(object)value;
@@ -0,0 +1,37 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter mapping to an object but also handles when an empty array is send
/// </summary>
/// <typeparam name="T"></typeparam>
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
{
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
/// <inheritdoc />
public override T? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.StartArray:
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
return default;
case JsonTokenType.StartObject:
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
};
return default;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, (object?)value, options);
}
}
@@ -172,6 +172,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return true; return true;
} }
if (objectType.IsDefined(typeof(FlagsAttribute)))
{
var intValue = int.Parse(value);
result = Enum.ToObject(objectType, intValue);
return true;
}
try try
{ {
// If no explicit mapping is found try to parse string // If no explicit mapping is found try to parse string
@@ -0,0 +1,31 @@
using System;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute for allowing specifying a JsonConverter with constructor parameters
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConverterCtorAttribute : JsonConverterAttribute
{
private readonly object[] _parameters;
private readonly Type _type;
/// <summary>
/// ctor
/// </summary>
public JsonConverterCtorAttribute(Type type, params object[] parameters)
{
_type = type;
_parameters = parameters;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert)
{
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Replace a value on a string property
/// </summary>
public class ReplaceConverter : JsonConverter<string>
{
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{
_replacementSets = replaceSets.Select(x =>
{
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
if (split.Length != 2)
throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
}
@@ -20,7 +20,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// </summary> /// </summary>
protected JsonDocument? _document; protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private readonly JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc /> /// <inheritdoc />
public bool IsJson { get; set; } public bool IsJson { get; set; }
@@ -31,6 +32,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public object? Underlying => throw new NotImplementedException(); public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null) public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{ {
@@ -42,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize(type, _serializerOptions); var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
return new CallResult<object>(result!); return new CallResult<object>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -65,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize<T>(_serializerOptions); var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
return new CallResult<T>(result!); return new CallResult<T>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -129,9 +145,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return value.Value.Deserialize<T>(_serializerOptions); return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
} }
catch { } catch { }
return default; return default;
} }
@@ -223,6 +240,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true; public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(): base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream) public async Task<CallResult> Read(Stream stream, bool bufferStream)
{ {
@@ -286,6 +317,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private ReadOnlyMemory<byte> _bytes; private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor() : base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data) public CallResult Read(ReadOnlyMemory<byte> data)
{ {
@@ -315,7 +360,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override string GetOriginalString() => public override string GetOriginalString() =>
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead // NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
#if NETSTANDARD2_0 #if NETSTANDARD2_0
Encoding.UTF8.GetString(_bytes.ToArray()); Encoding.UTF8.GetString(_bytes.ToArray());
#else #else
+5 -5
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<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>8.4.0</PackageVersion> <PackageVersion>8.8.0</PackageVersion>
<AssemblyVersion>8.4.0</AssemblyVersion> <AssemblyVersion>8.8.0</AssemblyVersion>
<FileVersion>8.4.0</FileVersion> <FileVersion>8.8.0</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>
@@ -20,7 +20,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+3 -2
View File
@@ -160,7 +160,7 @@ namespace CryptoExchange.Net
} }
/// <summary> /// <summary>
/// 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 statically stored so it is guaranteed to be unique
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static int NextId() => Interlocked.Increment(ref _lastId); public static int NextId() => Interlocked.Increment(ref _lastId);
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net
{ {
var randomChars = new char[length]; var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else #else
@@ -261,6 +261,7 @@ namespace CryptoExchange.Net
if (price != null) if (price != null)
{ {
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value); adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice; adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue) if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{ {
+9 -4
View File
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&"; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
} }
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
@@ -111,7 +111,8 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
} }
} }
return formData.ToString();
return formData.ToString()!;
} }
/// <summary> /// <summary>
@@ -286,6 +287,7 @@ namespace CryptoExchange.Net
httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
} }
} }
uriBuilder.Query = httpValueCollection.ToString(); uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri; return uriBuilder.Uri;
} }
@@ -333,6 +335,7 @@ namespace CryptoExchange.Net
httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
} }
} }
uriBuilder.Query = httpValueCollection.ToString(); uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri; return uriBuilder.Uri;
} }
@@ -344,7 +347,7 @@ namespace CryptoExchange.Net
/// <param name="name"></param> /// <param name="name"></param>
/// <param name="value"></param> /// <param name="value"></param>
/// <returns></returns> /// <returns></returns>
public static Uri AddQueryParmeter(this Uri uri, string name, string value) public static Uri AddQueryParameter(this Uri uri, string name, string value)
{ {
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query); var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
@@ -366,7 +369,7 @@ namespace CryptoExchange.Net
{ {
using var decompressedStream = new MemoryStream(); using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream); deflateStream.CopyTo(decompressedStream);
@@ -435,6 +438,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IWithdrawalRestClient)client(x)!); services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T))) if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawRestClient)client(x)!); services.AddTransient(x => (IWithdrawRestClient)client(x)!);
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!); services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
string BaseAddress { get; } string BaseAddress { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary> /// <summary>
/// Format a base and quote asset to an exchange accepted symbol /// Format a base and quote asset to an exchange accepted symbol
/// </summary> /// </summary>
@@ -31,5 +37,12 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="credentials"></param> /// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials; void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api credentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
} }
} }
@@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param> /// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param> /// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null); void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
} }
} }
@@ -19,7 +19,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
int CurrentSubscriptions { get; } int CurrentSubscriptions { get; }
/// <summary> /// <summary>
/// Incoming data kpbs /// Incoming data Kbps
/// </summary> /// </summary>
double IncomingKbps { get; } double IncomingKbps { get; }
/// <summary> /// <summary>
@@ -105,7 +105,7 @@ namespace CryptoExchange.Net.Interfaces
Task StopAsync(); Task StopAsync();
/// <summary> /// <summary>
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled /// Get the average price that a market order would fill at at the current order book state. This is no guarantee that an order of that quantity would actually be filled
/// at that price since between this calculation and the order placement the book might have changed. /// at that price since between this calculation and the order placement the book might have changed.
/// </summary> /// </summary>
/// <param name="quantity">The quantity in base asset to fill</param> /// <param name="quantity">The quantity in base asset to fill</param>
@@ -115,7 +115,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state. /// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed. /// This is no guarantee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
/// </summary> /// </summary>
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param> /// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
/// <param name="type">The type</param> /// <param name="type">The type</param>
+6 -1
View File
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
event Func<Task> OnReconnected; event Func<Task> OnReconnected;
/// <summary> /// <summary>
/// Get reconntion url /// Get reconnection url
/// </summary> /// </summary>
Func<Task<Uri?>>? GetReconnectionUrl { get; set; } Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
@@ -93,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task CloseAsync(); Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
} }
} }
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net
{
/// <summary>
/// Helpers for client libraries
/// </summary>
public static class LibraryHelpers
{
/// <summary>
/// Client order id separator
/// </summary>
public const string ClientOrderIdSeparator = "JK";
/// <summary>
/// Apply broker id to a client order id
/// </summary>
/// <param name="clientOrderId"></param>
/// <param name="brokerId"></param>
/// <param name="maxLength"></param>
/// <param name="allowValueAdjustment"></param>
/// <returns></returns>
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustment)
{
var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length;
if ((clientOrderId?.Length + reservedLength) > maxLength)
return clientOrderId!;
if (!string.IsNullOrEmpty(clientOrderId))
{
if (allowValueAdjustment)
clientOrderId = brokerId + ClientOrderIdSeparator + clientOrderId;
return clientOrderId!;
}
else
{
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeparator, maxLength);
}
return clientOrderId;
}
}
}
@@ -33,8 +33,9 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _receiveLoopStoppedWithException; private static readonly Action<ILogger, int, Exception?> _receiveLoopStoppedWithException;
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?> _noDataReceiveTimeoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged; private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
static CryptoExchangeWebSocketClientLoggingExtension() static CryptoExchangeWebSocketClientLoggingExtension()
{ {
@@ -168,8 +169,8 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(1026, "StartingTaskForNoDataReceivedCheck"), new EventId(1026, "StartingTaskForNoDataReceivedCheck"),
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}"); "[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>( _noDataReceiveTimeoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Debug, LogLevel.Warning,
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");
@@ -180,9 +181,14 @@ namespace CryptoExchange.Net.Logging.Extensions
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>( _socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace, LogLevel.Trace,
new EventId(1028, "SocketProcessingStateChanged"), new EventId(1029, "SocketProcessingStateChanged"),
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}"); "[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
_socketPingTimeout = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(1030, "SocketPingTimeout"),
"[Sckt {Id}] ping frame timeout; reconnecting socket");
} }
public static void SocketConnecting( public static void SocketConnecting(
@@ -350,7 +356,7 @@ namespace CryptoExchange.Net.Logging.Extensions
public static void SocketNoDataReceiveTimoutReconnect( public static void SocketNoDataReceiveTimoutReconnect(
this ILogger logger, int socketId, TimeSpan? timeSpan) this ILogger logger, int socketId, TimeSpan? timeSpan)
{ {
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null); _noDataReceiveTimeoutReconnect(logger, socketId, timeSpan, null);
} }
public static void SocketProcessingStateChanged( public static void SocketProcessingStateChanged(
@@ -358,5 +364,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_socketProcessingStateChanged(logger, socketId, prevState, newState, null); _socketProcessingStateChanged(logger, socketId, prevState, newState, null);
} }
public static void SocketPingTimeout(
this ILogger logger, int socketId)
{
_socketPingTimeout(logger, socketId, null);
}
} }
} }
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache; private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit; private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit; private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
static RestApiClientLoggingExtensions() static RestApiClientLoggingExtensions()
{ {
@@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>( _restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
LogLevel.Debug, LogLevel.Debug,
new EventId(4002, "RestApifailedToSyncTime"), new EventId(4002, "RestApiFailedToSyncTime"),
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}"); "[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
_restApiNoApiCredentials = LoggerMessage.Define<int, string>( _restApiNoApiCredentials = LoggerMessage.Define<int, string>(
@@ -84,6 +84,12 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Trace, LogLevel.Trace,
new EventId(4011, "RestApiCacheNotHit"), new EventId(4011, "RestApiCacheNotHit"),
"Cache not hit for key {Key}"); "Cache not hit for key {Key}");
_restApiCancellationRequested = LoggerMessage.Define<int?>(
LogLevel.Debug,
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user");
} }
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error) public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
@@ -145,5 +151,9 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_restApiCacheNotHit(logger, key, null); _restApiCacheNotHit(logger, key, null);
} }
public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
{
_restApiCancellationRequested(logger, requestId, null);
}
} }
} }
@@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused; private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged; private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
private static readonly Action<ILogger, int, string?, Exception?> _failedReconnectProcessing; private static readonly Action<ILogger, int, string?, Exception?> _failedReconnectProcessing;
private static readonly Action<ILogger, int, Exception?> _unkownExceptionWhileProcessingReconnection; private static readonly Action<ILogger, int, Exception?> _unknownExceptionWhileProcessingReconnection;
private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails; private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails;
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError; private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending; private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _closingNoMoreSubscriptions; private static readonly Action<ILogger, int, Exception?> _closingNoMoreSubscriptions;
private static readonly Action<ILogger, int, int, int, Exception?> _addingNewSubscription; private static readonly Action<ILogger, int, int, int, Exception?> _addingNewSubscription;
private static readonly Action<ILogger, int, Exception?> _nothingToResubscribeCloseConnection; private static readonly Action<ILogger, int, Exception?> _nothingToResubscribeCloseConnection;
private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndRecoonect; private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndReconnect;
private static readonly Action<ILogger, int, Exception?> _authenticationSucceeded; private static readonly Action<ILogger, int, Exception?> _authenticationSucceeded;
private static readonly Action<ILogger, int, string?, Exception?> _failedRequestRevitalization; private static readonly Action<ILogger, int, string?, Exception?> _failedRequestRevitalization;
private static readonly Action<ILogger, int, Exception?> _allSubscriptionResubscribed; private static readonly Action<ILogger, int, Exception?> _allSubscriptionResubscribed;
@@ -55,15 +55,15 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2002, "FailedReconnectProcessing"), new EventId(2002, "FailedReconnectProcessing"),
"[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again"); "[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again");
_unkownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>( _unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
LogLevel.Warning, LogLevel.Warning,
new EventId(2003, "UnkownExceptionWhileProcessingReconnection"), new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again"); "[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>( _webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
LogLevel.Warning, LogLevel.Warning,
new EventId(2004, "WebSocketErrorCode"), new EventId(2004, "WebSocketErrorCode"),
"[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCdoe}, details: {Details}"); "[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCode}, details: {Details}");
_webSocketError = LoggerMessage.Define<int, string?>( _webSocketError = LoggerMessage.Define<int, string?>(
LogLevel.Warning, LogLevel.Warning,
@@ -145,7 +145,7 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2020, "NothingToResubscribe"), new EventId(2020, "NothingToResubscribe"),
"[Sckt {SocketId}] nothing to resubscribe, closing connection"); "[Sckt {SocketId}] nothing to resubscribe, closing connection");
_failedAuthenticationDisconnectAndRecoonect = LoggerMessage.Define<int>( _failedAuthenticationDisconnectAndReconnect = LoggerMessage.Define<int>(
LogLevel.Warning, LogLevel.Warning,
new EventId(2021, "FailedAuthentication"), new EventId(2021, "FailedAuthentication"),
"[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting"); "[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting");
@@ -183,7 +183,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData = LoggerMessage.Define<int, int, string>( _sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace, LogLevel.Trace,
new EventId(2028, "SendingData"), new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}"); "[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>( _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning, LogLevel.Warning,
@@ -206,9 +206,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_failedReconnectProcessing(logger, socketId, error, null); _failedReconnectProcessing(logger, socketId, error, null);
} }
public static void UnkownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e) public static void UnknownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e)
{ {
_unkownExceptionWhileProcessingReconnection(logger, socketId, e); _unknownExceptionWhileProcessingReconnection(logger, socketId, e);
} }
public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e) public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e)
@@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Logging.Extensions
} }
public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId) public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId)
{ {
_failedAuthenticationDisconnectAndRecoonect(logger, socketId, null); _failedAuthenticationDisconnectAndReconnect(logger, socketId, null);
} }
public static void AuthenticationSucceeded(this ILogger logger, int socketId) public static void AuthenticationSucceeded(this ILogger logger, int socketId)
{ {
@@ -62,7 +62,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(5005, "OrderBookStopping"), new EventId(5005, "OrderBookStopping"),
"{Api} order book {Symbol} stopping"); "{Api} order book {Symbol} stopping");
_orderBookStopped = LoggerMessage.Define<string, string>( _orderBookStopped = LoggerMessage.Define<string, string>(
LogLevel.Trace, LogLevel.Trace,
new EventId(5006, "OrderBookStopped"), new EventId(5006, "OrderBookStopped"),
@@ -97,7 +97,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(6012, "KlineTrackerConnectionRestored"), new EventId(6012, "KlineTrackerConnectionRestored"),
"Kline tracker for {Symbol} successfully resynchronized"); "Kline tracker for {Symbol} successfully resynchronized");
_tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>( _tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug, LogLevel.Debug,
new EventId(6013, "KlineTrackerStatusChanged"), new EventId(6013, "KlineTrackerStatusChanged"),
@@ -32,44 +32,51 @@ namespace CryptoExchange.Net.Objects
/// Wait for the AutoResetEvent to be set /// Wait for the AutoResetEvent to be set
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default) public async Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{ {
lock (_waits) CancellationTokenRegistration registration = default;
try
{ {
if (_signaled) Task<bool> waiter = _completed;
lock (_waits)
{ {
if(_reset) if (_signaled)
_signaled = false;
return _completed;
}
else
{
if (ct.IsCancellationRequested)
return _completed;
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (timeout.HasValue)
{ {
var timeoutSource = new CancellationTokenSource(timeout.Value); if (_reset)
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct); _signaled = false;
ct = cancellationSource.Token;
} }
else if (!ct.IsCancellationRequested)
var registration = ct.Register(() =>
{ {
lock (_waits) var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (timeout.HasValue)
{ {
tcs.TrySetResult(false); var timeoutSource = new CancellationTokenSource(timeout.Value);
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
// Not the cleanest but it works ct = cancellationSource.Token;
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
} }
}, useSynchronizationContext: false);
_waits.Enqueue(tcs); registration = ct.Register(() =>
return tcs.Task; {
lock (_waits)
{
tcs.TrySetResult(false);
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false);
_waits.Enqueue(tcs);
waiter = tcs.Task;
}
} }
return await waiter.ConfigureAwait(false);
}
finally
{
registration.Dispose();
} }
} }
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(byte[] x, byte[] y) public int Compare(byte[]? x, byte[]? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
+1 -1
View File
@@ -89,7 +89,7 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// Create a new error result /// Create a new error result
/// </summary> /// </summary>
/// <param name="error">The erro rto return</param> /// <param name="error">The error to return</param>
public CallResult(Error error) : this(default, null, error) { } public CallResult(Error error) : this(default, null, error) { }
/// <summary> /// <summary>
+19 -3
View File
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.Objects using CryptoExchange.Net.Attributes;
namespace CryptoExchange.Net.Objects
{ {
/// <summary> /// <summary>
/// What to do when a request would exceed the rate limit /// What to do when a request would exceed the rate limit
@@ -92,7 +94,7 @@
/// <summary> /// <summary>
/// Disposed /// Disposed
/// </summary> /// </summary>
Diposed Disposed
} }
/// <summary> /// <summary>
@@ -215,7 +217,7 @@
/// </summary> /// </summary>
FixedDelay, FixedDelay,
/// <summary> /// <summary>
/// Backof policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5 /// Backoff policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5
/// </summary> /// </summary>
ExponentialBackoff ExponentialBackoff
} }
@@ -235,4 +237,18 @@
Cache Cache
} }
/// <summary>
/// Type of exchange
/// </summary>
public enum ExchangeType
{
/// <summary>
/// Centralized
/// </summary>
CEX,
/// <summary>
/// Decentralized
/// </summary>
DEX
}
} }
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.Objects.Options
{ {
/// <summary> /// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// </summary> /// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public TEnvironment Environment { get; set; } public TEnvironment Environment { get; set; }
@@ -52,6 +52,15 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public TimeSpan? ConnectDelayAfterRateLimited { get; set; } public TimeSpan? ConnectDelayAfterRateLimited { get; set; }
/// <summary>
/// The buffer size to use for receiving data. Leave unset to use the default buffer size.
/// </summary>
/// <remarks>
/// Only specify this if you are creating a significant amount of connections and understand the typical message length we receive from the exchange.
/// Setting this too low can increase memory consumption and allocations.
/// </remarks>
public int? ReceiveBufferSize { get; set; }
/// <summary> /// <summary>
/// Create a copy of this options /// Create a copy of this options
/// </summary> /// </summary>
@@ -72,6 +81,7 @@ namespace CryptoExchange.Net.Objects.Options
item.RequestTimeout = RequestTimeout; item.RequestTimeout = RequestTimeout;
item.RateLimitingBehaviour = RateLimitingBehaviour; item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimiterEnabled = RateLimiterEnabled; item.RateLimiterEnabled = RateLimiterEnabled;
item.ReceiveBufferSize = ReceiveBufferSize;
return item; return item;
} }
} }
@@ -0,0 +1,29 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<T> where T : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public T? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}
/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
}
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(string x, string y) public int Compare(string? x, string? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
@@ -156,7 +156,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddSecondsString(string key, DateTime value) public void AddSecondsString(string key, DateTime value)
{ {
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.Objects
public void AddOptionalSecondsString(string key, DateTime? value) public void AddOptionalSecondsString(string key, DateTime? value)
{ {
if (value != null) if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -187,7 +187,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value) public void AddEnumAsInt<T>(string key, T value)
{ {
var stringVal = EnumConverter.GetString(value); var stringVal = EnumConverter.GetString(value)!;
Add(key, int.Parse(stringVal)!); Add(key, int.Parse(stringVal)!);
} }
@@ -25,8 +25,7 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public bool Authenticated { get; set; } public bool Authenticated { get; set; }
// Formatting
// Formating
/// <summary> /// <summary>
/// The body format for this request /// The body format for this request
@@ -58,9 +58,38 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{ {
if (!_definitions.TryGetValue(method + path, out var def)) if (!_definitions.TryGetValue(identifier, out var def))
{ {
def = new RequestDefinition(path, method) def = new RequestDefinition(path, method)
{ {
@@ -73,7 +102,7 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false PreventCaching = preventCaching ?? false
}; };
_definitions.TryAdd(method + path, def); _definitions.TryAdd(identifier, def);
} }
return def; return def;
+36 -11
View File
@@ -12,7 +12,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary> /// <summary>
/// The timestamp the data was received /// The timestamp the data was received
/// </summary> /// </summary>
public DateTime Timestamp { get; set; } public DateTime ReceiveTime { get; set; }
/// <summary>
/// The timestamp of the data as specified by the server. Note that the server time and client time might not be 100% in sync so this value might not be fully comparable to local time.
/// </summary>
public DateTime? DataTime { get; set; }
/// <summary> /// <summary>
/// The stream producing the update /// The stream producing the update
@@ -42,29 +47,32 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType) public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime receiveTimestamp, SocketUpdateType? updateType)
{ {
Data = data; Data = data;
StreamId = streamId; StreamId = streamId;
Symbol = symbol; Symbol = symbol;
OriginalData = originalData; OriginalData = originalData;
Timestamp = timestamp; ReceiveTime = receiveTimestamp;
UpdateType = updateType; UpdateType = updateType;
} }
/// <summary> /// <summary>
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and Timestamp will be copied over /// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and ReceivedTimestamp will be copied over
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data) public DataEvent<K> As<K>(K data)
{ {
return new DataEvent<K>(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType); return new DataEvent<K>(data, StreamId, Symbol, OriginalData, ReceiveTime, UpdateType)
{
DataTime = DataTime
};
} }
/// <summary> /// <summary>
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
@@ -72,11 +80,14 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string? symbol) public DataEvent<K> As<K>(K data, string? symbol)
{ {
return new DataEvent<K>(data, StreamId, symbol, OriginalData, Timestamp, UpdateType); return new DataEvent<K>(data, StreamId, symbol, OriginalData, ReceiveTime, UpdateType)
{
DataTime = DataTime
};
} }
/// <summary> /// <summary>
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
@@ -86,7 +97,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType) public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
{ {
return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType); return new DataEvent<K>(data, streamId, symbol, OriginalData, ReceiveTime, updateType)
{
DataTime = DataTime
};
} }
/// <summary> /// <summary>
@@ -98,10 +112,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data) public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data)
{ {
return new ExchangeEvent<K>(exchange, this.As<K>(data)); return new ExchangeEvent<K>(exchange, this.As<K>(data))
{
DataTime = DataTime
};
} }
/// <summary> /// <summary>
/// Specify the symbol /// Specify the symbol
/// </summary> /// </summary>
@@ -135,6 +151,15 @@ namespace CryptoExchange.Net.Objects.Sockets
return this; return this;
} }
/// <summary>
/// Specify the data timestamp
/// </summary>
public DataEvent<T> WithDataTimestamp(DateTime? timestamp)
{
DataTime = timestamp;
return this;
}
/// <summary> /// <summary>
/// Create a CallResult from this DataEvent /// Create a CallResult from this DataEvent
/// </summary> /// </summary>
@@ -41,7 +41,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary> /// <summary>
/// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting.
/// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the diconnect /// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the disconnect
/// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes. /// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes.
/// </summary> /// </summary>
public event Action<TimeSpan> ConnectionRestored public event Action<TimeSpan> ConnectionRestored
@@ -41,7 +41,7 @@ namespace CryptoExchange.Net.Objects.Sockets
public ApiProxy? Proxy { get; set; } public ApiProxy? Proxy { get; set; }
/// <summary> /// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket /// The maximum time of no data received before considering the connection lost and closing/reconnecting the socket
/// </summary> /// </summary>
public TimeSpan? Timeout { get; set; } public TimeSpan? Timeout { get; set; }
@@ -57,13 +57,18 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary> /// <summary>
/// What to do when rate limit is reached /// What to do when rate limit is reached
/// </summary> /// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } public RateLimitingBehaviour RateLimitingBehavior { get; set; }
/// <summary> /// <summary>
/// Encoding for sending/receiving data /// Encoding for sending/receiving data
/// </summary> /// </summary>
public Encoding Encoding { get; set; } = Encoding.UTF8; public Encoding Encoding { get; set; } = Encoding.UTF8;
/// <summary>
/// The buffer size to use for receiving data
/// </summary>
public int? ReceiveBufferSize { get; set; } = null;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
+2 -2
View File
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow; TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{ {
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms"); Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
TimeSyncState.TimeOffset = TimeSpan.Zero; TimeSyncState.TimeOffset = TimeSpan.Zero;
} }
else else
{ {
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms"); Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
TimeSyncState.TimeOffset = offset; TimeSyncState.TimeOffset = offset;
} }
} }
@@ -17,7 +17,7 @@
/// <summary> /// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the echange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
/// </summary> /// </summary>
public class TradeEnvironment public class TradeEnvironment
{ {
@@ -74,8 +74,7 @@ namespace CryptoExchange.Net.OrderBook
/// <summary> /// <summary>
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes /// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
/// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the /// the current level 25 ask out of the top 25, should the level 26 entry be removed from the book or does the server handle this
/// server handle this
/// </summary> /// </summary>
protected bool _strictLevels; protected bool _strictLevels;
@@ -250,6 +249,7 @@ namespace CryptoExchange.Net.OrderBook
// Clear any previous messages // Clear any previous messages
while (_processQueue.TryDequeue(out _)) { } while (_processQueue.TryDequeue(out _)) { }
_processBuffer.Clear(); _processBuffer.Clear();
_bookSet = false; _bookSet = false;
@@ -407,7 +407,7 @@ namespace CryptoExchange.Net.OrderBook
/// <summary> /// <summary>
/// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot /// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot
/// received from a socket subcription /// received from a socket subscription
/// </summary> /// </summary>
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param> /// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
/// <param name="askList">List of asks</param> /// <param name="askList">List of asks</param>
@@ -618,6 +618,7 @@ namespace CryptoExchange.Net.OrderBook
var bid = book.bids.Count() > i ? book.bids.ElementAt(i): null; var bid = book.bids.Count() > i ? book.bids.ElementAt(i): null;
stringBuilder.AppendLine($"[{ask?.Quantity.ToString(CultureInfo.InvariantCulture),14}] {ask?.Price.ToString(CultureInfo.InvariantCulture),14} | {bid?.Price.ToString(CultureInfo.InvariantCulture),-14} [{bid?.Quantity.ToString(CultureInfo.InvariantCulture),-14}]"); stringBuilder.AppendLine($"[{ask?.Quantity.ToString(CultureInfo.InvariantCulture),14}] {ask?.Price.ToString(CultureInfo.InvariantCulture),14} | {bid?.Price.ToString(CultureInfo.InvariantCulture),-14} [{bid?.Quantity.ToString(CultureInfo.InvariantCulture),-14}]");
} }
return stringBuilder.ToString(); return stringBuilder.ToString();
} }
@@ -636,6 +637,7 @@ namespace CryptoExchange.Net.OrderBook
_queueEvent.Set(); _queueEvent.Set();
// Clear queue // Clear queue
while (_processQueue.TryDequeue(out _)) { } while (_processQueue.TryDequeue(out _)) { }
_processBuffer.Clear(); _processBuffer.Clear();
_bookSet = false; _bookSet = false;
DoReset(); DoReset();
@@ -732,7 +734,7 @@ namespace CryptoExchange.Net.OrderBook
var (prevBestBid, prevBestAsk) = BestOffers; var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks); ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
if (!_asks.Any() || !_bids.Any()) if (_asks.Count == 0 || _bids.Count == 0)
return; return;
if (_asks.First().Key < _bids.First().Key) if (_asks.First().Key < _bids.First().Key)
@@ -843,9 +845,9 @@ namespace CryptoExchange.Net.OrderBook
internal class DescComparer<T> : IComparer<T> internal class DescComparer<T> : IComparer<T>
{ {
public int Compare(T x, T y) public int Compare(T? x, T? y)
{ {
return Comparer<T>.Default.Compare(y, x); return Comparer<T>.Default.Compare(y!, x!);
} }
} }
} }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Apply guard per connection /// Apply guard per connection
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()); public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
/// <summary> /// <summary>
/// Apply guard per API key /// Apply guard per API key
/// </summary> /// </summary>
@@ -32,9 +32,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
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 readonly RateLimitWindowType _windowType;
private double? _decayRate; private readonly double? _decayRate;
private int? _connectionWeight; private readonly int? _connectionWeight;
private readonly Func<RequestDefinition, string, string?, string> _keySelector; private readonly Func<RequestDefinition, string, string?, string> _keySelector;
/// <inheritdoc /> /// <inheritdoc />
@@ -90,7 +90,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{ {
foreach(var filter in _filters) foreach(var filter in _filters)
{ {
@@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
if (type == RateLimitItemType.Connection) if (type == RateLimitItemType.Connection)
requestWeight = _connectionWeight ?? requestWeight; requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker)) if (!_trackers.TryGetValue(key, out var tracker))
{ {
tracker = CreateTracker(); tracker = CreateTracker();
@@ -116,7 +116,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{ {
foreach (var filter in _filters) foreach (var filter in _filters)
{ {
@@ -127,7 +127,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
if (type == RateLimitItemType.Connection) if (type == RateLimitItemType.Connection)
requestWeight = _connectionWeight ?? requestWeight; requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey) + keySuffix;
var tracker = _trackers[key]; var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight); tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current); return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{ {
if (type != Type) if (type != Type)
return LimitCheck.NotApplicable; return LimitCheck.NotApplicable;
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{ {
return RateLimitState.NotApplied; return RateLimitState.NotApplied;
} }
@@ -19,7 +19,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Endpoint limit per API key /// Endpoint limit per API key
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, 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 + key);
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
@@ -53,9 +53,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{ {
var key = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker)) if (!_trackers.TryGetValue(key, out var tracker))
{ {
tracker = CreateTracker(); tracker = CreateTracker();
@@ -70,9 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{ {
var key = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey) + keySuffix;
var tracker = _trackers[key]; var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight); tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(_limit, _period, tracker.Current); return RateLimitState.Applied(_limit, _period, tracker.Current);
@@ -53,9 +53,10 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="requestWeight">Request weight</param> /// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</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, string? 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, string? keySuffix, 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
@@ -68,8 +69,10 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="baseAddress">The host address</param> /// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</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, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct); Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
} }
} }
@@ -25,8 +25,9 @@ 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>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns> /// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight); LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
/// <summary> /// <summary>
/// Apply the request to this guard with the specified weight /// Apply the request to this guard with the specified weight
@@ -36,7 +37,8 @@ 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>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns> /// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight); RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
} }
} }
@@ -37,14 +37,14 @@ namespace CryptoExchange.Net.RateLimiting
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? 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, string? keySuffix, CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true; 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, keySuffix, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
@@ -69,7 +69,9 @@ namespace CryptoExchange.Net.RateLimiting
RequestDefinition definition, RequestDefinition definition,
string host, string host,
string? apiKey, string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour, RateLimitingBehaviour rateLimitingBehaviour,
string? keySuffix,
CancellationToken ct) CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
@@ -77,7 +79,7 @@ namespace CryptoExchange.Net.RateLimiting
_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, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
@@ -93,12 +95,12 @@ namespace CryptoExchange.Net.RateLimiting
} }
} }
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) private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
{ {
foreach (var guard in guards) foreach (var guard in guards)
{ {
// Check if a wait is needed for this guard // Check if a wait is needed for this guard
var result = guard.Check(type, definition, host, apiKey, requestWeight); var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix);
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail) if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
{ {
// Delay is needed and limit behaviour is to fail the request // Delay is needed and limit behaviour is to fail the request
@@ -125,14 +127,14 @@ namespace CryptoExchange.Net.RateLimiting
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
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, keySuffix, ct).ConfigureAwait(false);
} }
} }
// Apply the weight on each guard // Apply the weight on each guard
foreach (var guard in guards) foreach (var guard in guards)
{ {
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight); var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix);
if (result.IsApplied) if (result.IsApplied)
{ {
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period)); RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
@@ -38,7 +38,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}"); $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}"); $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
@@ -41,7 +41,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}"); $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, Ratelimit: {Limit}"); $"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
@@ -102,7 +102,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
} }
throw new Exception("Request not possible to execute with current rate limit guard. " + throw new Exception("Request not possible to execute with current rate limit guard. " +
$" Request weight: {requestWeight}, Ratelimit: {Limit}"); $" Request weight: {requestWeight}, RateLimit: {Limit}");
} }
} }
} }
+1 -1
View File
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.Requests
} }
/// <inheritdoc /> /// <inheritdoc />
public Uri Uri => _request.RequestUri; public Uri Uri => _request.RequestUri!;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
+34 -22
View File
@@ -17,28 +17,7 @@ namespace CryptoExchange.Net.Requests
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
if (client == null) if (client == null)
{ client = CreateClient(proxy, requestTimeout);
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
_httpClient = client; _httpClient = client;
} }
@@ -51,5 +30,38 @@ namespace CryptoExchange.Net.Requests
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
} }
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
{
_httpClient = CreateClient(proxy, requestTimeout);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var client = new HttpClient(handler)
{
Timeout = requestTimeout
};
return client;
}
} }
} }
@@ -5,6 +5,14 @@
/// </summary> /// </summary>
public enum SharedKlineInterval public enum SharedKlineInterval
{ {
/// <summary>
/// 1 min
/// </summary>
OneMinute = 60,
/// <summary>
/// 3 min
/// </summary>
ThreeMinutes = 60 * 3,
/// <summary> /// <summary>
/// 5 min /// 5 min
/// </summary> /// </summary>
@@ -14,10 +22,34 @@
/// </summary> /// </summary>
FifteenMinutes = 60 * 15, FifteenMinutes = 60 * 15,
/// <summary> /// <summary>
/// Thirty minutes
/// </summary>
ThirtyMinutes = 60 * 30,
/// <summary>
/// 1 hour /// 1 hour
/// </summary> /// </summary>
OneHour = 60 * 60, OneHour = 60 * 60,
/// <summary> /// <summary>
/// 2 hours
/// </summary>
TwoHours = 60 * 60 * 2,
/// <summary>
/// 4 hours
/// </summary>
FourHours = 60 * 60 * 4,
/// <summary>
/// 6 hours
/// </summary>
SixHours = 60 * 60 * 6,
/// <summary>
/// 8 hours
/// </summary>
EightHours = 60 * 60 * 8,
/// <summary>
/// 12 hours
/// </summary>
TwelveHours = 60 * 60 * 12,
/// <summary>
/// 1 day /// 1 day
/// </summary> /// </summary>
OneDay = 60 * 60 * 24, OneDay = 60 * 60 * 24,
@@ -21,9 +21,10 @@ namespace CryptoExchange.Net.SharedApis
evnt.StreamId, evnt.StreamId,
evnt.Symbol, evnt.Symbol,
evnt.OriginalData, evnt.OriginalData,
evnt.Timestamp, evnt.ReceiveTime,
evnt.UpdateType) evnt.UpdateType)
{ {
DataTime = evnt.DataTime;
Exchange = exchange; Exchange = exchange;
} }
@@ -10,7 +10,7 @@ namespace CryptoExchange.Net.SharedApis
public class ExchangeParameters public class ExchangeParameters
{ {
private readonly List<ExchangeParameter> _parameters; private readonly List<ExchangeParameter> _parameters;
private static List<ExchangeParameter> _staticParameters = new List<ExchangeParameter>(); private readonly static List<ExchangeParameter> _staticParameters = new List<ExchangeParameter>();
/// <summary> /// <summary>
/// ctor /// ctor
@@ -132,7 +132,6 @@ namespace CryptoExchange.Net.SharedApis
NextPageToken = nextPageToken; NextPageToken = nextPageToken;
} }
/// <summary> /// <summary>
/// Copy the ExchangeWebResult to a new data type /// Copy the ExchangeWebResult to a new data type
/// </summary> /// </summary>
@@ -60,8 +60,8 @@ namespace CryptoExchange.Net.SharedApis
} }
else else
{ {
if (param.Names.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -113,13 +113,13 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (!string.IsNullOrEmpty(param.Name)) if (!string.IsNullOrEmpty(param.Name))
{ {
if (typeof(T).GetProperty(param.Name).GetValue(request, null) == null) if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
} }
else else
{ {
if (param.Names.All(x => typeof(T).GetProperty(param.Name).GetValue(request, null) == null)) if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -31,9 +31,17 @@ namespace CryptoExchange.Net.SharedApis
{ {
SupportIntervals = new[] SupportIntervals = new[]
{ {
SharedKlineInterval.OneMinute,
SharedKlineInterval.ThreeMinutes,
SharedKlineInterval.FiveMinutes, SharedKlineInterval.FiveMinutes,
SharedKlineInterval.FifteenMinutes, SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.ThirtyMinutes,
SharedKlineInterval.OneHour, SharedKlineInterval.OneHour,
SharedKlineInterval.TwoHours,
SharedKlineInterval.FourHours,
SharedKlineInterval.SixHours,
SharedKlineInterval.EightHours,
SharedKlineInterval.TwelveHours,
SharedKlineInterval.OneDay, SharedKlineInterval.OneDay,
SharedKlineInterval.OneWeek, SharedKlineInterval.OneWeek,
SharedKlineInterval.OneMonth SharedKlineInterval.OneMonth
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
public override string ToString(string exchange) public override string ToString(string exchange)
{ {
var sb = new StringBuilder(base.ToString(exchange)); var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Supported limit values: [{(SupportedLimits == null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (Name != null) if (Name != null)
return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}";
return $"[{ValueType.Name}] {string.Join(" / ", Names)}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}";
} }
} }
} }
@@ -22,10 +22,17 @@ namespace CryptoExchange.Net.SharedApis
{ {
SupportIntervals = new[] SupportIntervals = new[]
{ {
SharedKlineInterval.OneMinute,
SharedKlineInterval.ThreeMinutes,
SharedKlineInterval.FiveMinutes, SharedKlineInterval.FiveMinutes,
SharedKlineInterval.FifteenMinutes, SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.ThirtyMinutes,
SharedKlineInterval.OneHour, SharedKlineInterval.OneHour,
SharedKlineInterval.FifteenMinutes, SharedKlineInterval.TwoHours,
SharedKlineInterval.FourHours,
SharedKlineInterval.SixHours,
SharedKlineInterval.EightHours,
SharedKlineInterval.TwelveHours,
SharedKlineInterval.OneDay, SharedKlineInterval.OneDay,
SharedKlineInterval.OneWeek, SharedKlineInterval.OneWeek,
SharedKlineInterval.OneMonth SharedKlineInterval.OneMonth
@@ -46,6 +46,10 @@
/// </summary> /// </summary>
public int? PriceDecimals { get; set; } public int? PriceDecimals { get; set; }
/// <summary> /// <summary>
/// The max amount of significant figures to use for price. For example with value of 5 these values are valid: 0.00001, 0.12300, 123.53, 12345, but this is not: 12345.1
/// </summary>
public int? PriceSignificantFigures { get; set; }
/// <summary>
/// Whether the symbol is currently available for trading /// Whether the symbol is currently available for trading
/// </summary> /// </summary>
public bool Trading { get; set; } public bool Trading { get; set; }
@@ -1,12 +1,13 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System; using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// A symbol representation based on a base and quote asset /// A symbol representation based on a base and quote asset
/// </summary> /// </summary>
public class SharedSymbol public record SharedSymbol
{ {
/// <summary> /// <summary>
/// The base asset of the symbol /// The base asset of the symbol
@@ -5,6 +5,7 @@ using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -32,6 +33,7 @@ namespace CryptoExchange.Net.Sockets
internal static int _lastStreamId; internal static int _lastStreamId;
private static readonly object _streamIdLock = new(); private static readonly object _streamIdLock = new();
private static readonly ArrayPool<byte> _receiveBufferPool = ArrayPool<byte>.Shared;
private readonly AsyncResetEvent _sendEvent; private readonly AsyncResetEvent _sendEvent;
private readonly ConcurrentQueue<SendItem> _sendBuffer; private readonly ConcurrentQueue<SendItem> _sendBuffer;
@@ -46,14 +48,15 @@ namespace CryptoExchange.Net.Sockets
private bool _disposed; private bool _disposed;
private ProcessState _processState; private ProcessState _processState;
private DateTime _lastReconnectTime; private DateTime _lastReconnectTime;
private string _baseAddress; private readonly string _baseAddress;
private int _reconnectAttempt; private int _reconnectAttempt;
private readonly int _receiveBufferSize;
private const int _receiveBufferSize = 1048576; private const int _defaultReceiveBufferSize = 1048576;
private const int _sendBufferSize = 4096; private const int _sendBufferSize = 4096;
/// <summary> /// <summary>
/// Received messages, the size and the timstamp /// Received messages, the size and the timestamp
/// </summary> /// </summary>
protected readonly List<ReceiveItem> _receivedMessages; protected readonly List<ReceiveItem> _receivedMessages;
@@ -96,7 +99,7 @@ namespace CryptoExchange.Net.Sockets
{ {
UpdateReceivedMessages(); UpdateReceivedMessages();
if (!_receivedMessages.Any()) if (_receivedMessages.Count == 0)
return 0; return 0;
return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000d / 3d); return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000d / 3d);
@@ -149,12 +152,19 @@ namespace CryptoExchange.Net.Sockets
_sendBuffer = new ConcurrentQueue<SendItem>(); _sendBuffer = new ConcurrentQueue<SendItem>();
_ctsSource = new CancellationTokenSource(); _ctsSource = new CancellationTokenSource();
_receivedMessagesLock = new object(); _receivedMessagesLock = new object();
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? _defaultReceiveBufferSize;
_closeSem = new SemaphoreSlim(1, 1); _closeSem = new SemaphoreSlim(1, 1);
_socket = CreateSocket(); _socket = CreateSocket();
_baseAddress = $"{Uri.Scheme}://{Uri.Host}"; _baseAddress = $"{Uri.Scheme}://{Uri.Host}";
} }
/// <inheritdoc />
public void UpdateProxy(ApiProxy? proxy)
{
Parameters.Proxy = proxy;
}
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<CallResult> ConnectAsync() public virtual async Task<CallResult> ConnectAsync()
{ {
@@ -189,9 +199,12 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null) if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy); SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true; socket.Options.CollectHttpResponseDetails = true;
#endif #endif
#if NET9_0_OR_GREATER
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
#endif
} }
catch (PlatformNotSupportedException) catch (PlatformNotSupportedException)
{ {
@@ -210,7 +223,7 @@ namespace CryptoExchange.Net.Sockets
if (Parameters.RateLimiter != null) if (Parameters.RateLimiter != null)
{ {
var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false); var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult(new ClientRateLimitError("Connection limit reached")); return new CallResult(new ClientRateLimitError("Connection limit reached"));
} }
@@ -229,13 +242,13 @@ namespace CryptoExchange.Net.Sockets
if (e is WebSocketException we) if (e is WebSocketException we)
{ {
#if (NET6_0_OR_GREATER) #if (NET6_0_OR_GREATER)
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
{ {
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#else #else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead // Try to read 429 from the message instead
if (we.Message.Contains("429")) if (we.Message.Contains("429"))
@@ -243,7 +256,7 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#endif #endif
} }
return new CallResult(new CantConnectError()); return new CallResult(new CantConnectError());
@@ -287,7 +300,7 @@ namespace CryptoExchange.Net.Sockets
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
} }
// Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected // Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected
var initialDelay = GetReconnectDelay(); var initialDelay = GetReconnectDelay();
await Task.Delay(initialDelay).ConfigureAwait(false); await Task.Delay(initialDelay).ConfigureAwait(false);
@@ -435,8 +448,8 @@ namespace CryptoExchange.Net.Sockets
{ {
// Wait until we receive close confirmation // Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false); await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5)) if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
break; // Wait for max 5 seconds, then just abort the connection break; // Wait for max 1 second, then just abort the connection
} }
} }
} }
@@ -482,7 +495,7 @@ namespace CryptoExchange.Net.Sockets
{ {
try try
{ {
if (!_sendBuffer.Any()) if (_sendBuffer.IsEmpty)
await _sendEvent.WaitAsync(ct: _ctsSource.Token).ConfigureAwait(false); await _sendEvent.WaitAsync(ct: _ctsSource.Token).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -499,7 +512,7 @@ namespace CryptoExchange.Net.Sockets
{ {
try try
{ {
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false); var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
{ {
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
@@ -557,8 +570,8 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
private async Task ReceiveLoopAsync() private async Task ReceiveLoopAsync()
{ {
var buffer = new ArraySegment<byte>(new byte[_receiveBufferSize]); byte[] rentedBuffer = _receiveBufferPool.Rent(_receiveBufferSize);
var received = 0; var buffer = new ArraySegment<byte>(rentedBuffer);
try try
{ {
while (true) while (true)
@@ -574,19 +587,30 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false);
received += receiveResult.Count;
lock (_receivedMessagesLock) lock (_receivedMessagesLock)
_receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count)); _receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count));
} }
catch (OperationCanceledException) catch (OperationCanceledException ex)
{ {
if (ex.InnerException?.InnerException?.Message.Contains("KeepAliveTimeout") == true)
{
// Specific case that the websocket connection got closed because of a ping frame timeout
// Unfortunately doesn't seem to be a nicer way to catch
_logger.SocketPingTimeout(Id);
}
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
// canceled // canceled
break; break;
} }
catch (Exception wse) catch (Exception wse)
{ {
// Connection closed unexpectedly if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested)
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false); // Connection closed unexpectedly
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false) if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
break; break;
@@ -598,14 +622,14 @@ namespace CryptoExchange.Net.Sockets
if (_socket.State == WebSocketState.CloseReceived) if (_socket.State == WebSocketState.CloseReceived)
{ {
// Close received means it server initiated, we should send a confirmation and close the socket // Close received means it server initiated, we should send a confirmation and close the socket
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
if (_closeTask?.IsCompleted != false) if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
} }
else else
{ {
// Means the socket is now closed and we were the one initiating it // Means the socket is now closed and we were the one initiating it
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
} }
break; break;
@@ -620,7 +644,7 @@ namespace CryptoExchange.Net.Sockets
// Write the data to a memory stream to be reassembled later // Write the data to a memory stream to be reassembled later
if (multipartStream == null) if (multipartStream == null)
multipartStream = new MemoryStream(); multipartStream = new MemoryStream();
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
else else
{ {
@@ -628,13 +652,13 @@ namespace CryptoExchange.Net.Sockets
{ {
// Received a complete message and it's not multi part // Received a complete message and it's not multi part
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count); _logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false); await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array!, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
} }
else else
{ {
// Received the end of a multipart message, write to memory stream for reassembling // Received the end of a multipart message, write to memory stream for reassembling
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count); _logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
break; break;
@@ -658,11 +682,11 @@ namespace CryptoExchange.Net.Sockets
if (multiPartMessage) if (multiPartMessage)
{ {
// When the connection gets interupted we might not have received a full message // When the connection gets interrupted we might not have received a full message
if (receiveResult?.EndOfMessage == true) if (receiveResult?.EndOfMessage == true)
{ {
_logger.SocketReassembledMessage(Id, multipartStream!.Length); _logger.SocketReassembledMessage(Id, multipartStream!.Length);
// Get the underlying buffer of the memorystream holding the written data and delimit it (GetBuffer return the full array, not only the written part) // Get the underlying buffer of the memory stream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false); await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false);
} }
else else
@@ -684,12 +708,13 @@ namespace CryptoExchange.Net.Sockets
} }
finally finally
{ {
_receiveBufferPool.Return(rentedBuffer, true);
_logger.SocketReceiveLoopFinished(Id); _logger.SocketReceiveLoopFinished(Id);
} }
} }
/// <summary> /// <summary>
/// Proccess a stream message /// Process a stream message
/// </summary> /// </summary>
/// <param name="type"></param> /// <param name="type"></param>
/// <param name="data"></param> /// <param name="data"></param>
@@ -721,6 +746,7 @@ namespace CryptoExchange.Net.Sockets
_ = ReconnectAsync().ConfigureAwait(false); _ = ReconnectAsync().ConfigureAwait(false);
return; return;
} }
try try
{ {
await Task.Delay(500, _ctsSource.Token).ConfigureAwait(false); await Task.Delay(500, _ctsSource.Token).ConfigureAwait(false);
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Callback after query /// Callback after query
/// </summary> /// </summary>
public Action<CallResult>? Callback { get; set; } public Action<SocketConnection, CallResult>? Callback { get; set; }
} }
} }
+5
View File
@@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool Completed { get; set; } public bool Completed { get; set; }
/// <summary>
/// Timeout for the request
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
/// <summary> /// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response /// and each symbol receives it's own confirmation response
+24 -12
View File
@@ -11,6 +11,8 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using System.Threading; using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -141,7 +143,7 @@ namespace CryptoExchange.Net.Sockets
public DateTime? DisconnectTime { get; set; } public DateTime? DisconnectTime { get; set; }
/// <summary> /// <summary>
/// Tag for identificaion /// Tag for identification
/// </summary> /// </summary>
public string Tag { get; set; } public string Tag { get; set; }
@@ -212,7 +214,7 @@ namespace CryptoExchange.Net.Sockets
private readonly IByteMessageAccessor _accessor; private readonly IByteMessageAccessor _accessor;
/// <summary> /// <summary>
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry. /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similar. Not necessary.
/// </summary> /// </summary>
protected Task? periodicTask; protected Task? periodicTask;
@@ -284,7 +286,7 @@ namespace CryptoExchange.Net.Sockets
foreach (var query in _listeners.OfType<Query>().ToList()) foreach (var query in _listeners.OfType<Query>().ToList())
{ {
query.Fail(new WebError("Connection interupted")); query.Fail(new WebError("Connection interrupted"));
_listeners.Remove(query); _listeners.Remove(query);
} }
} }
@@ -309,7 +311,7 @@ namespace CryptoExchange.Net.Sockets
foreach (var query in _listeners.OfType<Query>().ToList()) foreach (var query in _listeners.OfType<Query>().ToList())
{ {
query.Fail(new WebError("Connection interupted")); query.Fail(new WebError("Connection interrupted"));
_listeners.Remove(query); _listeners.Remove(query);
} }
} }
@@ -338,7 +340,7 @@ namespace CryptoExchange.Net.Sockets
{ {
foreach (var query in _listeners.OfType<Query>().ToList()) foreach (var query in _listeners.OfType<Query>().ToList())
{ {
query.Fail(new WebError("Connection interupted")); query.Fail(new WebError("Connection interrupted"));
_listeners.Remove(query); _listeners.Remove(query);
} }
} }
@@ -367,7 +369,7 @@ namespace CryptoExchange.Net.Sockets
} }
catch(Exception ex) catch(Exception ex)
{ {
_logger.UnkownExceptionWhileProcessingReconnection(SocketId, ex); _logger.UnknownExceptionWhileProcessingReconnection(SocketId, ex);
_ = _socket.ReconnectAsync().ConfigureAwait(false); _ = _socket.ReconnectAsync().ConfigureAwait(false);
} }
}); });
@@ -390,13 +392,13 @@ namespace CryptoExchange.Net.Sockets
} }
/// <summary> /// <summary>
/// Handler for whenever a request is rate limited and rate limit behaviour is set to fail /// Handler for whenever a request is rate limited and rate limit behavior is set to fail
/// </summary> /// </summary>
/// <param name="requestId"></param> /// <param name="requestId"></param>
/// <returns></returns> /// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId) protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -425,7 +427,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="requestId">Id of the request sent</param> /// <param name="requestId">Id of the request sent</param>
protected virtual Task HandleRequestSentAsync(int requestId) protected virtual Task HandleRequestSentAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -437,7 +439,7 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask; return Task.CompletedTask;
} }
query.IsSend(ApiClient.ClientOptions.RequestTimeout); query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -583,6 +585,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
/// <summary>
/// Update the proxy setting and reconnect
/// </summary>
/// <param name="proxy">New proxy setting</param>
public async Task UpdateProxy(ApiProxy? proxy)
{
_socket.UpdateProxy(proxy);
await TriggerReconnectAsync().ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Close the connection /// Close the connection
/// </summary> /// </summary>
@@ -988,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="interval">How often</param> /// <param name="interval">How often</param>
/// <param name="queryDelegate">Method returning the query to send</param> /// <param name="queryDelegate">Method returning the query to send</param>
/// <param name="callback">The callback for processing the response</param> /// <param name="callback">The callback for processing the response</param>
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
if (queryDelegate == null) if (queryDelegate == null)
throw new ArgumentNullException(nameof(queryDelegate)); throw new ArgumentNullException(nameof(queryDelegate));
@@ -1020,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
callback?.Invoke(result); callback?.Invoke(this, result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -172,7 +172,7 @@ namespace CryptoExchange.Net.Testing.Comparers
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
} }
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0") if ((propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0")
return; return;
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary))) if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
@@ -378,7 +378,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal) if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString())) if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
} }
} }
@@ -69,11 +69,11 @@ namespace CryptoExchange.Net.Testing.Comparers
} }
else if (jsonObject!.Type == JTokenType.Array) else if (jsonObject!.Type == JTokenType.Array)
{ {
var jObjs = (JArray)jsonObject; var jArray = (JArray)jsonObject;
if (resultData is IEnumerable list) if (resultData is IEnumerable list)
{ {
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
foreach (var jObj in jObjs) foreach (var jObj in jArray)
{ {
if (!enumerator.MoveNext()) if (!enumerator.MoveNext())
{ {
@@ -105,7 +105,7 @@ namespace CryptoExchange.Net.Testing.Comparers
int i = 0; int i = 0;
foreach (var item in jObj.Children()) foreach (var item in jObj.Children())
{ {
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null) if (arrayProp != null)
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
i++; i++;
@@ -123,7 +123,7 @@ namespace CryptoExchange.Net.Testing.Comparers
{ {
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
int i = 0; int i = 0;
foreach (var item in jObjs.Children()) foreach (var item in jArray.Children())
{ {
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null) if (arrayProp != null)
@@ -196,7 +196,7 @@ namespace CryptoExchange.Net.Testing.Comparers
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
} }
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0") if ((propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0")
return; return;
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary))) if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
@@ -211,7 +211,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object) if (dictProp.Value.Type == JTokenType.Object)
{ {
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties); CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties);
} }
else else
{ {
@@ -227,10 +227,10 @@ namespace CryptoExchange.Net.Testing.Comparers
if (propValue.Type != JTokenType.Array) if (propValue.Type != JTokenType.Array)
return; return;
var jObjs = (JArray)propValue; var jArray = (JArray)propValue;
var list = (IEnumerable)propertyValue; var list = (IEnumerable)propertyValue;
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
foreach (JToken jtoken in jObjs) foreach (JToken jToken in jArray)
{ {
var moved = enumerator.MoveNext(); var moved = enumerator.MoveNext();
if (!moved) if (!moved)
@@ -241,9 +241,9 @@ namespace CryptoExchange.Net.Testing.Comparers
// Custom converter for the type, skip // Custom converter for the type, skip
continue; continue;
if (jtoken.Type == JTokenType.Object) if (jToken.Type == JTokenType.Object)
{ {
foreach (var subProp in ((JObject)jtoken).Properties()) foreach (var subProp in ((JObject)jToken).Properties())
{ {
if (ignoreProperties?.Contains(subProp.Name) == true) if (ignoreProperties?.Contains(subProp.Name) == true)
continue; continue;
@@ -251,7 +251,7 @@ namespace CryptoExchange.Net.Testing.Comparers
CheckObject(method, subProp, enumerator.Current, ignoreProperties); CheckObject(method, subProp, enumerator.Current, ignoreProperties);
} }
} }
else if (jtoken.Type == JTokenType.Array) else if (jToken.Type == JTokenType.Array)
{ {
var resultObj = enumerator.Current; var resultObj = enumerator.Current;
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
@@ -262,11 +262,11 @@ namespace CryptoExchange.Net.Testing.Comparers
continue; continue;
int i = 0; int i = 0;
foreach (var item in jtoken.Children()) foreach (var item in jToken.Children())
{ {
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null) if (arrayProp != null)
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties); CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
i++; i++;
} }
@@ -274,10 +274,10 @@ namespace CryptoExchange.Net.Testing.Comparers
else else
{ {
var value = enumerator.Current; var value = enumerator.Current;
if (value == default && ((JValue)jtoken).Type != JTokenType.Null) if (value == default && ((JValue)jToken).Type != JTokenType.Null)
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}"); throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jToken}");
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!); CheckValues(method, propertyName!, propertyType, (JValue)jToken, value!);
} }
} }
} }
@@ -298,11 +298,11 @@ namespace CryptoExchange.Net.Testing.Comparers
} }
else if (propValue.Type == JTokenType.Array) else if (propValue.Type == JTokenType.Array)
{ {
var jObjs = (JArray)propValue; var jArray = (JArray)propValue;
if (propertyValue is IEnumerable list) if (propertyValue is IEnumerable list)
{ {
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
foreach (var jObj in jObjs) foreach (var jObj in jArray)
{ {
if (!enumerator.MoveNext()) if (!enumerator.MoveNext())
{ {
@@ -348,9 +348,9 @@ namespace CryptoExchange.Net.Testing.Comparers
{ {
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault())); var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
int i = 0; int i = 0;
foreach (var item in jObjs.Children()) foreach (var item in jArray.Children())
{ {
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null) if (arrayProp != null)
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
i++; i++;
@@ -5,8 +5,11 @@ namespace CryptoExchange.Net.Testing
{ {
internal class EnumValueTraceListener : TraceListener internal class EnumValueTraceListener : TraceListener
{ {
public override void Write(string message) public override void Write(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -14,8 +17,11 @@ namespace CryptoExchange.Net.Testing
throw new Exception("Enum null error: " + message); throw new Exception("Enum null error: " + message);
} }
public override void WriteLine(string message) public override void WriteLine(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -25,5 +25,7 @@ namespace CryptoExchange.Net.Testing.Implementations
_request.RequestId = requestId; _request.RequestId = requestId;
return _request; return _request;
} }
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
} }
} }
@@ -92,5 +92,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public Task ReconnectAsync() => throw new NotImplementedException(); public Task ReconnectAsync() => throw new NotImplementedException();
public void Dispose() { } public void Dispose() { }
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
} }
} }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Testing
/// Base class for executing REST API integration tests /// Base class for executing REST API integration tests
/// </summary> /// </summary>
/// <typeparam name="TClient">Client type</typeparam> /// <typeparam name="TClient">Client type</typeparam>
public abstract class RestIntergrationTest<TClient> public abstract class RestIntegrationTest<TClient>
{ {
/// <summary> /// <summary>
/// Get a client instance /// Get a client instance
@@ -113,7 +113,6 @@ namespace CryptoExchange.Net.Testing
if (lastMessage == null) if (lastMessage == null)
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
var lastMessageJson = JToken.Parse(lastMessage); var lastMessageJson = JToken.Parse(lastMessage);
var expectedJson = JToken.Parse(line.Substring(2)); var expectedJson = JToken.Parse(line.Substring(2));
foreach(var item in expectedJson) foreach(var item in expectedJson)
@@ -133,7 +132,9 @@ namespace CryptoExchange.Net.Testing
overrideValue = lastMessageJson[prop.Name]?.Value<decimal>().ToString(); overrideValue = lastMessageJson[prop.Name]?.Value<decimal>().ToString();
} }
else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true) else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true)
{
throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}"); throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}");
}
} }
// TODO check objects and arrays // TODO check objects and arrays
+14 -10
View File
@@ -150,9 +150,9 @@ namespace CryptoExchange.Net.Testing
/// </summary> /// </summary>
/// <typeparam name="TClient"></typeparam> /// <typeparam name="TClient"></typeparam>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
public static void CheckForMissingRestInterfaces<TClient>() public static void CheckForMissingRestInterfaces<TClient>(string[]? excludeInterfaces = null)
{ {
CheckForMissingInterfaces(typeof(TClient), typeof(Task)); CheckForMissingInterfaces(typeof(TClient), typeof(Task), excludeInterfaces);
} }
/// <summary> /// <summary>
@@ -160,28 +160,32 @@ namespace CryptoExchange.Net.Testing
/// </summary> /// </summary>
/// <typeparam name="TClient"></typeparam> /// <typeparam name="TClient"></typeparam>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
public static void CheckForMissingSocketInterfaces<TClient>() public static void CheckForMissingSocketInterfaces<TClient>(string[]? excludeInterfaces = null)
{ {
CheckForMissingInterfaces(typeof(TClient), typeof(Task<CallResult<UpdateSubscription>>)); CheckForMissingInterfaces(typeof(TClient), typeof(Task<CallResult<UpdateSubscription>>), excludeInterfaces);
} }
private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes) private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes, string[]? excludeInterfaces = null)
{ {
var assembly = Assembly.GetAssembly(clientType); var assembly = Assembly.GetAssembly(clientType);
var interfaceType = clientType.GetInterface("I" + clientType.Name); var interfaceType = clientType.GetInterface("I" + clientType.Name);
var clientInterfaces = assembly.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared")); var clientInterfaces = assembly!.GetTypes()
.Where(t => t.Name.StartsWith("I" + clientType.Name)
&& !t.Name.EndsWith("Shared")
&& (excludeInterfaces?.Contains(t.Name) != true));
foreach (var clientInterface in clientInterfaces) foreach (var clientInterface in clientInterfaces)
{ {
var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && t != clientInterface); var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && !t.IsInterface && t != clientInterface);
foreach (var implementation in implementations) foreach (var implementation in implementations)
{ {
int methods = 0; int methods = 0;
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType))) foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
{ {
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()); var interfaceMethod =
if (interfaceMethod == null) clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray())
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}"); ?? clientInterface.GetInterfaces().Select(x => x.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray())).FirstOrDefault()
?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
methods++; methods++;
} }
@@ -94,7 +94,7 @@ namespace CryptoExchange.Net.Trackers.Klines
IEnumerable<SharedKline> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); IEnumerable<SharedKline> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
/// <summary> /// <summary>
/// Get statitistics on the klines /// Get statistics on the klines
/// </summary> /// </summary>
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param> /// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param> /// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Trackers.Klines
/// <summary> /// <summary>
/// The internal data structure /// The internal data structure
/// </summary> /// </summary>
protected readonly Dictionary<DateTime, SharedKline> _data = new Dictionary<DateTime, SharedKline>(); protected readonly SortedDictionary<DateTime, SharedKline> _data = new SortedDictionary<DateTime, SharedKline>();
/// <summary> /// <summary>
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
/// </summary> /// </summary>
@@ -90,7 +90,7 @@ namespace CryptoExchange.Net.Trackers.Trades
IEnumerable<SharedTrade> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); IEnumerable<SharedTrade> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
/// <summary> /// <summary>
/// Get statitistics on the trades /// Get statistics on the trades
/// </summary> /// </summary>
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param> /// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param> /// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
@@ -163,7 +163,7 @@ namespace CryptoExchange.Net.Trackers.Trades
Period = period; Period = period;
} }
private TradesStats GetStats(IEnumerable<SharedTrade> trades) private static TradesStats GetStats(IEnumerable<SharedTrade> trades)
{ {
if (!trades.Any()) if (!trades.Any())
return new TradesStats(); return new TradesStats();
@@ -350,7 +350,8 @@ namespace CryptoExchange.Net.Trackers.Trades
_data.Add(item); _data.Add(item);
} }
_firstTimestamp = _data.Min(v => v.Timestamp); if (_data.Count != 0)
_firstTimestamp = _data.Min(v => v.Timestamp);
ApplyWindow(false); ApplyWindow(false);
} }
@@ -430,7 +431,6 @@ namespace CryptoExchange.Net.Trackers.Trades
SetSyncStatus(); SetSyncStatus();
} }
private void HandleConnectionLost() private void HandleConnectionLost()
{ {
_logger.TradeTrackerConnectionLost(SymbolName); _logger.TradeTrackerConnectionLost(SymbolName);

Some files were not shown because too many files have changed in this diff Show More