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

Compare commits

..

125 Commits

Author SHA1 Message Date
Jkorf f2cf70b02f Updated to version 8.2.0 2024-11-06 14:00:23 +01:00
Jkorf 9ff417bba8 Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token 2024-11-06 13:56:33 +01:00
Jkorf 6b43d08a4d Added support for not allowing duplicate subscription topics on the same websocket connection 2024-11-06 11:39:11 +01:00
Jkorf 39bf7fe9b9 Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T> 2024-11-06 11:20:37 +01:00
Jkorf b5893c3b60 Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable 2024-11-06 11:20:11 +01:00
Jkorf 15657ba683 Updated to version 8.1.1 2024-11-01 10:38:30 +01:00
Jkorf 1aed9f0c67 Fixed System.Text.Json ArrayConverter not passing serializer options to nested deserialization, fixed creating new serializer options each time a JsonConverter attribute is encountered 2024-11-01 10:34:07 +01:00
Jkorf 17f1560310 Fixed socket connections trying to authenticated connection when it's marked as dedicated request connection even when no authentication is needed 2024-11-01 09:38:01 +01:00
Jkorf 41de0a3150 Update index.html 2024-10-28 16:14:54 +01:00
Jkorf 3e410be611 Update index.html 2024-10-28 16:11:01 +01:00
Jkorf be75449e4a Updated examples, added trackers example 2024-10-28 15:38:25 +01:00
Jkorf b1b05c8f6b Added catch around HttpClientHandler.AutomaticDecompression setting as it's not support on Blazor WASM 2024-10-28 13:41:58 +01:00
Jkorf a0e588c3de Updated to version 8.1.0 2024-10-28 10:44:45 +01:00
Jan Korf 9e86a08327 Trackers (#218)
Fix for intermittently failing rate limiting test
Added ConnectionId to RequestDefinition to correctly handle connection and path rate limiting configuration
Added ValidateMessage method to websocket Query object to filter messages even though it is matched to the query based on the  ListenIdentifier
Added KlineTracker and TradeTracker implementation
2024-10-28 10:36:19 +01:00
Jkorf ed007b5272 Added overload for Create method in OrderBookFactory using SharedSymbol 2024-10-23 14:04:10 +02:00
Jkorf bdd5526244 Added Side to SharedTrade model 2024-10-23 14:01:17 +02:00
Jkorf ce35e30688 Updated documentation and examples 2024-10-22 16:20:27 +02:00
Jkorf b40f72b1b0 Added Crypto.com reference 2024-10-22 15:37:56 +02:00
Jkorf 31a6cf285b Doc fix 2024-10-22 11:56:59 +02:00
Jkorf 1842f4fda0 Set ApiCredentials in the client specific options to prevent unknown client credentials when using SetApiCredentials method on client 2024-10-22 10:15:17 +02:00
Jkorf 7a58902ab6 Made SharedFuturesTicker last/high/low price properties nullable 2024-10-22 09:54:44 +02:00
Jkorf 3cb91296ca Added DoHandleReset method for websocket subscriptions 2024-10-22 09:47:14 +02:00
Jkorf 130ed40580 Comment 2024-10-21 16:29:56 +02:00
Jkorf 94cb2caf0b Added System.Text.Json ArrayConverter Write implementation 2024-10-15 10:51:19 +02:00
Jkorf 917d060827 Updated to version 8.0.3 2024-10-14 14:13:38 +02:00
JKorf c58bc2be07 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-10-12 13:06:28 +02:00
JKorf ff3356e2b4 Added Authenticated property on base client and shared client 2024-10-12 13:06:22 +02:00
Jkorf 79434c7be5 Implemented GetValues System.Text.Json in message accessor 2024-10-11 16:02:17 +02:00
Jkorf 168dabc11f Added fallback for unparsable value in System.Text.Json NumberStringConverter 2024-10-09 15:42:11 +02:00
Jkorf 71ee263683 Added support for duplicate array indexes in System.Text.Json ArrayConverter 2024-10-09 15:41:43 +02:00
Jkorf 7239b9c289 docs 2024-10-09 10:25:57 +02:00
Jkorf 84d36544e4 Updated to version 8.0.2 2024-10-09 08:49:19 +02:00
Jkorf a71f57ae7f Updated dependency versions, including System.Text.Json containing a vulnerability fix 2024-10-09 08:46:15 +02:00
Jkorf 6e5bcd5e9a Some small doc fixes 2024-10-07 15:30:09 +02:00
Jkorf 4131e563c3 Added Coinbase reference, updated examples 2024-10-07 15:20:12 +02:00
Jkorf 613766dbca Updated to version 8.0.1 2024-10-07 13:03:08 +02:00
Jkorf 23b07d709e Added testing check for year 1 datetime 2024-10-07 12:57:23 +02:00
Jkorf bbbdac2fd3 Added ToRfc3339String extension method for DateTime 2024-10-07 12:57:04 +02:00
Jkorf c614b7869c Added check for datetime year 1 to be deserialized as null 2024-10-04 18:50:27 +02:00
Jkorf 1f31e4a9d7 Added cached lib versions properties 2024-10-04 18:49:58 +02:00
Jkorf 6cb6cd6b11 Note 2024-10-03 16:33:00 +02:00
Jkorf 17ffec329f Fixed typo 2024-09-27 15:15:09 +02:00
Jkorf 7a3927ef49 Add parameters documentation for shared clients 2024-09-27 15:13:50 +02:00
Jkorf c1b0437c93 Updated examples 2024-09-27 13:53:33 +02:00
Jkorf 23e947f258 Updated to version 8.0.0 2024-09-27 10:55:29 +02:00
Jan Korf b8686d60b9 Shared exchange functionality (#214) 2024-09-27 09:17:44 +02:00
Jonnern 5d3de52da6 feature: Handle error 429 when connecting websocket (#213)
* feature: Handle error 429 when connecting websocket

* Add preprocessor directive for NET6_0_OR_GREATER when checking for connection rate limit
2024-09-24 12:50:59 +02:00
Jonnern fee18fd183 Dispose ClientWebSocket before creating a new (#212) 2024-09-17 11:59:58 +02:00
JKorf 3a43d461a3 Fixed workflow dotnet version 2024-08-29 16:45:34 +02:00
Jkorf 5f409efad3 Updated dotnet version unit tests 2024-08-29 14:01:59 +02:00
JKorf cc1f0796fe Updated to version 7.11.2 2024-08-28 19:17:06 +02:00
JKorf b1cd9b5412 Fixed exception being thrown when waiting was canceled during rate limiting 2024-08-28 19:10:08 +02:00
Jonnern 42003a0247 Fix issue where SemaphoreSlim is released twice in RateLimitGate (#210) 2024-08-28 12:25:09 +02:00
JKorf d89c2bde94 Updated to version 7.11.1 2024-08-25 18:41:33 +02:00
JKorf 3e6bdaafc6 Improved closing logic websockets 2024-08-25 18:38:37 +02:00
JKorf 93e4722a81 Added testing checks for JsonInclude attribute for internal properties 2024-08-08 09:20:26 +02:00
JKorf 355ecb03da Updated to version 7.11.0 2024-08-07 18:42:00 +02:00
JKorf 994c527c1d Fixed warning 2024-08-07 18:39:01 +02:00
JKorf 69b2e2045e Fixed System.Text.Json tests not correctly checking capitalization 2024-08-07 16:49:12 +02:00
JKorf 7fde8bf5da Added converters/handling for values too big to fit decimal 2024-08-07 14:00:50 +02:00
JKorf 637070a7ae Fixed some warnings, added support for number deserialization when requesting string in STJ MessageAccessor.GetValue<T> 2024-08-06 15:37:15 +02:00
JKorf 7be75f72a7 Add check for null string to decimal converter 2024-08-05 21:45:01 +02:00
JKorf e3fece41f3 Small test fixes 2024-08-05 16:13:08 +02:00
JKorf 87b0c8d7a2 Docs 2024-08-02 13:28:42 +02:00
JKorf ca9a711f22 Support too large numbers for long value in NumberStringConverter, fall back to string 2024-08-02 12:07:15 +02:00
JKorf 27597bc994 Fix test 2024-08-02 08:59:14 +02:00
JKorf 776d75170d Fixed websocket client trying to unsubscribe subscription when the connection will be closed anyway 2024-08-02 08:59:04 +02:00
JKorf 949780a9ad Removed SecureString usage throughout the library, removed some object allocations, removed some unused extension methods 2024-08-01 22:43:06 +02:00
JKorf 2f64cd9f05 Add BitMart reference 2024-07-30 22:14:25 +02:00
JKorf 185dfeb6fb Added ParseString to EnumConverter for manual calling 2024-07-28 22:39:38 +02:00
JKorf 68067d6258 Updated to version 7.10.0 2024-07-26 14:29:07 +02:00
JKorf b309deb0c4 Small fixed/improvements. Added deflate stream method 2024-07-25 18:47:33 +02:00
JKorf e1dafdf0dd Added AddSecondsString and AddOptionalSecondsString method to ParameterCollection 2024-07-24 10:22:35 +02:00
JKorf fd7b5f0f0f Added RestIntergrationTest base class for running integration tests 2024-07-23 16:58:10 +02:00
JKorf 3b735d66fd Improved socket reconnect logic so unsubscribing while processing resubscribing won't result in a ghost subscription 2024-07-23 12:01:31 +02:00
JKorf 3cd505ac8b Fixed order book logging 2024-07-23 12:00:15 +02:00
JKorf 81d856d78d Fixed System.Text.Json array converter deserialization when skipping indexes, added NumberStringConverter 2024-07-23 12:00:08 +02:00
JKorf 11c1ad871a Docs 2024-07-19 10:15:21 +02:00
JKorf ffcb7db8ff Updated to version 7.9.0 2024-07-16 19:01:03 +02:00
JKorf 02432e5109 Added small performance improvements in SystemTextJsonMessageAccessor 2024-07-16 15:54:11 +02:00
JKorf a85bfb4432 Added support for requiring multiple responses for queries, fixed possible exception when closing connection, added ToString override DataEvent object 2024-07-16 15:23:09 +02:00
JKorf 17d85fdd85 Added WebCallResult As<T> methods for untyped WebCallResult 2024-07-16 08:35:19 +02:00
JKorf 5e0733d7f4 Fixed trying to authenticate dedicated connection when no credentials are set 2024-07-14 20:59:22 +02:00
JKorf 28d5287bd4 Changed logging extensions to public so overridden methods can access them 2024-07-11 19:19:35 +02:00
JKorf ef5097589a Test compare improvements 2024-07-10 16:57:30 +02:00
JKorf f287ec1fa4 Added some checks socket connections 2024-07-10 14:12:07 +02:00
JKorf 28da93af9d Test fixes 2024-07-10 10:18:58 +02:00
JKorf 0d5bdf5095 Bumped System.Text.Json package to 8.0.4 2024-07-10 10:09:36 +02:00
JKorf 8dac3d7aa6 Refactor to prevent usage of thread block ManualResetEvent in socket message handling 2024-07-09 16:50:08 +02:00
JKorf 6951f31be7 Fix ArrayConverter error for nullable types 2024-07-05 16:34:58 +02:00
JKorf 630f85ec49 Updated to version 7.8.0 2024-07-02 20:17:01 +02:00
JKorf 9ec4f2276f Updated single endpoint limit configuration, added LongConverter, updated SystemTextJsonComparer logic 2024-07-02 16:13:10 +02:00
JKorf 0a0c66541e Updated to version 7.7.3 2024-06-26 19:11:13 +02:00
JKorf bb4199620e Added caching docs 2024-06-26 15:33:31 +02:00
JKorf 8a83cd2cb8 Array comparison updates for unit tests 2024-06-26 15:13:21 +02:00
JKorf fcfeaf568f Fixed request ids not matching 2024-06-26 11:21:52 +02:00
JKorf 25567ea434 Added nullable int converter for System.Text.Json 2024-06-25 20:52:35 +02:00
JKorf 1ab85d4c26 Updated to version 7.7.2 2024-06-25 16:42:45 +02:00
JKorf be68115099 Fix for ratelimiting possibly creating negative waits 2024-06-25 16:14:09 +02:00
JKorf ff0550b0fb Updated to version 7.7.1 2024-06-23 22:40:13 +02:00
JKorf 1ab1e008fc Some fixes for caching 2024-06-23 22:39:54 +02:00
JKorf 6f30c72608 Updated to version 7.7.0 2024-06-23 15:38:40 +02:00
JKorf e927bc3d20 Close socket when connecting and authentication fails 2024-06-23 14:55:20 +02:00
JKorf 09ed7d1436 Small improvements 2024-06-23 14:41:59 +02:00
JKorf 6fed657ea6 Added ObjectStringConverter for double serialized objects 2024-06-21 16:49:02 +02:00
Jonnern 1555f8da0c Fix issue with rate limit guard waiting sub ms (#204) 2024-06-21 16:47:14 +02:00
JKorf 68b28fc875 Small improvements 2024-06-19 17:01:26 +02:00
JKorf 5d50d8cde8 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-06-17 17:29:57 +02:00
JKorf 9ff673d8be Fixed unhandled exception when websocket connection is interupted while waiting for ratelimit 2024-06-17 10:53:22 +02:00
JKorf 3e5a34fb56 Added dedicated request websocket connection support 2024-06-16 16:55:44 +02:00
JKorf 64ee50d98c Added SocketConnection as parameter to GetAuthenticationRequest socket api client 2024-06-14 14:06:57 +02:00
JKorf 6a105c6f8f Added support for caching GET requests 2024-06-13 16:29:02 +02:00
JKorf 287aadc720 Added CancellationToken support for websocket queries 2024-06-13 11:58:52 +02:00
JKorf 7229438a0b Docs 2024-06-12 21:42:20 +02:00
JKorf 444af98a15 Added GateIo reference 2024-06-12 19:23:34 +02:00
JKorf 70c6fa1bbb Fixed tests 2024-06-12 18:05:26 +02:00
JKorf d27f394b46 Removed HandleUpdatesBeforeConfirmation flag, allow messages to trigger listeners even if not confirmed and mark as confirmed then. Updated websocket reconnection delay handling 2024-06-12 16:56:06 +02:00
JKorf c8c98e13d0 Updated to version 7.6.0 2024-06-11 16:32:28 +02:00
Jan Korf 9fcd722991 Feature/body uri param split (#203)
* Added support for specifying seperate uri and body parameters
* Added support for different message and handling generic types on socket queries
* Split DataEvent.Topic into StreamId and Symbol properties
* Added support for negative time values parsing
* Added some helper methods for converting DataEvent to CallResult
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
* Updated some testing methods
2024-06-11 16:23:48 +02:00
JKorf 8080ecccc0 Added support for Patch requests, added SetBody to ParameterCollection for directly setting the request body 2024-06-04 09:54:24 +02:00
JKorf 4b6fa9a1b1 Delete release.yml 2024-06-02 09:38:23 +02:00
JKorf 0b6dbde7d4 docs 2024-06-01 22:56:43 +02:00
JKorf fe4d63ba75 Update index.html 2024-06-01 22:17:26 +02:00
JKorf 04bd3727ca Updated version 2024-05-07 09:56:59 +02:00
JKorf 7e6fcd03c2 Update release.yml 2024-05-07 09:50:22 +02:00
276 changed files with 13088 additions and 1259 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
-23
View File
@@ -1,23 +0,0 @@
name: Publish Nuget
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup .NET SDK
uses: actions/setup-dotnet@v1
- name: Build
run: dotnet build -c Release
- name: Test
run: dotnet test -c Release --no-build
- name: Pack nugets
run: dotnet pack -c Release --no-build --include-symbols -p:PackageVersion=${{github.event.release.name}} --output .
- name: Push Package to NuGet
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.nuget_api_key}} --source https://api.nuget.org/v3/index.json
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
null,
HttpMethod.Get,
new List<KeyValuePair<string, IEnumerable<string>>>(),
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.As<TestObject2>(result.Data.InnerData);
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
</ItemGroup>
<ItemGroup>
+6 -6
View File
@@ -51,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
// assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key.GetString() == "123");
Assert.That(options.ApiCredentials.Secret.GetString() == "456");
Assert.That(options.ApiCredentials.Key == "123");
Assert.That(options.ApiCredentials.Secret == "456");
}
[Test]
@@ -64,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert
Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456");
Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101");
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
}
[Test]
+32 -17
View File
@@ -176,12 +176,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(i == requests? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(!triggered);
}
@@ -201,7 +201,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimiting ? evnt != null : evnt == null);
}
@@ -243,12 +243,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(!triggered);
}
@@ -266,7 +266,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
@@ -302,16 +302,16 @@ namespace CryptoExchange.Net.UnitTests
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -328,7 +328,7 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
@@ -348,9 +348,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -365,10 +365,25 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
[Test]
public async Task ConnectionRateLimiterCancel()
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
}
}
@@ -58,9 +58,7 @@ namespace CryptoExchange.Net.UnitTests
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
Dictionary<string, string> result = null;
@@ -72,11 +70,10 @@ namespace CryptoExchange.Net.UnitTests
result = messageEvent.Data;
rstEvent.Set();
});
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj);
// act
socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000);
// assert
@@ -94,9 +91,7 @@ namespace CryptoExchange.Net.UnitTests
options.SubOptions.OutputOriginalData = enabled;
});
var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
string original = null;
@@ -107,9 +102,8 @@ namespace CryptoExchange.Net.UnitTests
original = messageEvent.OriginalData;
rstEvent.Set();
});
subObj.HandleUpdatesBeforeConfirmation = true;
sub.AddSubscription(subObj);
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 });
// act
socket.InvokeMessage(msgToSend);
@@ -204,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" }));
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" }));
await sub;
// assert
@@ -227,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" }));
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" }));
await sub;
// assert
@@ -5,6 +5,8 @@ using NUnit.Framework;
using System;
using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
namespace CryptoExchange.Net.UnitTests
{
@@ -163,6 +165,20 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
Assert.That(result == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
@@ -200,6 +216,79 @@ namespace CryptoExchange.Net.UnitTests
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterNumber(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[Test()]
public void TestArrayConverter()
{
var data = new Test()
{
Prop1 = 2,
Prop2 = null,
Prop3 = "123",
Prop3Again = "123",
Prop4 = null,
Prop5 = new Test2
{
Prop21 = 3,
Prop22 = "456"
},
Prop6 = new Test3
{
Prop31 = 4,
Prop32 = "789"
},
Prop7 = TestEnum.Two
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
Assert.That(deserialized.Prop1, Is.EqualTo(2));
Assert.That(deserialized.Prop2, Is.Null);
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
Assert.That(deserialized.Prop4, Is.Null);
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
}
}
public class STJDecimalObject
{
[JsonConverter(typeof(DecimalConverter))]
[JsonPropertyName("test")]
public decimal? Test { get; set; }
}
public class STJTimeObject
@@ -232,4 +321,42 @@ namespace CryptoExchange.Net.UnitTests
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; }
}
[JsonConverter(typeof(ArrayConverter))]
record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string Prop3 { get; set; }
[ArrayProperty(2)]
public string Prop3Again { get; set; }
[ArrayProperty(3)]
public string Prop4 { get; set; }
[ArrayProperty(4)]
public Test2 Prop5 { get; set; }
[ArrayProperty(5)]
public Test3 Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))]
public TestEnum? Prop7 { get; set; }
}
[JsonConverter(typeof(ArrayConverter))]
record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string Prop22 { get; set; }
}
record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string Prop32 { get; set; }
}
}
@@ -10,6 +10,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class SubResponse
{
[JsonProperty("action")]
public string Action { get; set; } = null!;
[JsonProperty("channel")]
public string Channel { get; set; } = null!;
@@ -19,6 +23,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
internal class UnsubResponse
{
[JsonProperty("action")]
public string Action { get; set; } = null!;
[JsonProperty("status")]
public string Status { get; set; } = null!;
}
@@ -29,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
ListenerIdentifiers = new HashSet<string> { channel };
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
}
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
private readonly Action<DataEvent<T>> _handler;
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
@@ -8,6 +8,7 @@ using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
@@ -55,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
@@ -68,11 +69,11 @@ namespace CryptoExchange.Net.UnitTests
{
}
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, IDictionary<string, object> uriParams, IDictionary<string, object> bodyParams, Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
{
}
public string GetKey() => _credentials.Key.GetString();
public string GetSecret() => _credentials.Secret.GetString();
public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret;
}
}
@@ -15,6 +15,7 @@ using System.Collections.Generic;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -138,7 +139,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
@@ -182,7 +183,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
@@ -1,131 +1,132 @@
using System;
using System.IO;
using System.Net.WebSockets;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
//using System;
//using System.IO;
//using System.Net.WebSockets;
//using System.Security.Authentication;
//using System.Text;
//using System.Threading.Tasks;
//using CryptoExchange.Net.Interfaces;
//using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestSocket: IWebsocket
{
public bool CanConnect { get; set; }
public bool Connected { get; set; }
//namespace CryptoExchange.Net.UnitTests.TestImplementations
//{
// public class TestSocket: IWebsocket
// {
// public bool CanConnect { get; set; }
// public bool Connected { get; set; }
public event Func<Task> OnClose;
#pragma warning disable 0067
public event Func<Task> OnReconnected;
public event Func<Task> OnReconnecting;
public event Func<int, Task> OnRequestRateLimited;
#pragma warning restore 0067
public event Func<int, Task> OnRequestSent;
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
public event Func<Exception, Task> OnError;
public event Func<Task> OnOpen;
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
// public event Func<Task> OnClose;
//#pragma warning disable 0067
// public event Func<Task> OnReconnected;
// public event Func<Task> OnReconnecting;
// public event Func<int, Task> OnRequestRateLimited;
//#pragma warning restore 0067
// public event Func<int, Task> OnRequestSent;
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
// public event Func<Exception, Task> OnError;
// public event Func<Task> OnOpen;
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
public int Id { get; }
public bool ShouldReconnect { get; set; }
public TimeSpan Timeout { get; set; }
public Func<string, string> DataInterpreterString { get; set; }
public Func<byte[], string> DataInterpreterBytes { get; set; }
public DateTime? DisconnectTime { get; set; }
public string Url { get; }
public bool IsClosed => !Connected;
public bool IsOpen => Connected;
public bool PingConnection { get; set; }
public TimeSpan PingInterval { get; set; }
public SslProtocols SSLProtocols { get; set; }
public Encoding Encoding { get; set; }
// public int Id { get; }
// public bool ShouldReconnect { get; set; }
// public TimeSpan Timeout { get; set; }
// public Func<string, string> DataInterpreterString { get; set; }
// public Func<byte[], string> DataInterpreterBytes { get; set; }
// public DateTime? DisconnectTime { get; set; }
// public string Url { get; }
// public bool IsClosed => !Connected;
// public bool IsOpen => Connected;
// public bool PingConnection { get; set; }
// public TimeSpan PingInterval { get; set; }
// public SslProtocols SSLProtocols { get; set; }
// public Encoding Encoding { get; set; }
public int ConnectCalls { get; private set; }
public bool Reconnecting { get; set; }
public string Origin { get; set; }
public int? RatelimitPerSecond { get; set; }
// public int ConnectCalls { get; private set; }
// public bool Reconnecting { get; set; }
// public string Origin { get; set; }
// public int? RatelimitPerSecond { get; set; }
public double IncomingKbps => throw new NotImplementedException();
// public double IncomingKbps => throw new NotImplementedException();
public Uri Uri => new Uri("");
// public Uri Uri => new Uri("");
public TimeSpan KeepAliveInterval { get; set; }
// public TimeSpan KeepAliveInterval { get; set; }
public static int lastId = 0;
public static object lastIdLock = new object();
// public static int lastId = 0;
// public static object lastIdLock = new object();
public TestSocket()
{
lock (lastIdLock)
{
Id = lastId + 1;
lastId++;
}
}
// public TestSocket()
// {
// lock (lastIdLock)
// {
// Id = lastId + 1;
// lastId++;
// }
// }
public Task<CallResult> ConnectAsync()
{
Connected = CanConnect;
ConnectCalls++;
if (CanConnect)
InvokeOpen();
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
}
// public Task<CallResult> ConnectAsync()
// {
// Connected = CanConnect;
// ConnectCalls++;
// if (CanConnect)
// InvokeOpen();
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
// }
public void Send(int requestId, string data, int weight)
{
if(!Connected)
throw new Exception("Socket not connected");
OnRequestSent?.Invoke(requestId);
}
// public bool Send(int requestId, string data, int weight)
// {
// if(!Connected)
// throw new Exception("Socket not connected");
// OnRequestSent?.Invoke(requestId);
// return true;
// }
public void Reset()
{
}
// public void Reset()
// {
// }
public Task CloseAsync()
{
Connected = false;
DisconnectTime = DateTime.UtcNow;
OnClose?.Invoke();
return Task.FromResult(0);
}
// public Task CloseAsync()
// {
// Connected = false;
// DisconnectTime = DateTime.UtcNow;
// OnClose?.Invoke();
// return Task.FromResult(0);
// }
public void SetProxy(string host, int port)
{
throw new NotImplementedException();
}
public void Dispose()
{
}
// public void SetProxy(string host, int port)
// {
// throw new NotImplementedException();
// }
// public void Dispose()
// {
// }
public void InvokeClose()
{
Connected = false;
DisconnectTime = DateTime.UtcNow;
Reconnecting = true;
OnClose?.Invoke();
}
// public void InvokeClose()
// {
// Connected = false;
// DisconnectTime = DateTime.UtcNow;
// Reconnecting = true;
// OnClose?.Invoke();
// }
public void InvokeOpen()
{
OnOpen?.Invoke();
}
// public void InvokeOpen()
// {
// OnOpen?.Invoke();
// }
public void InvokeMessage(string data)
{
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
}
// public void InvokeMessage(string data)
// {
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
// }
public void SetProxy(ApiProxy proxy)
{
throw new NotImplementedException();
}
// public void SetProxy(ApiProxy proxy)
// {
// throw new NotImplementedException();
// }
public void InvokeError(Exception error)
{
OnError?.Invoke(error);
}
public Task ReconnectAsync() => Task.CompletedTask;
}
}
// public void InvokeError(Exception error)
// {
// OnError?.Invoke(error);
// }
// public Task ReconnectAsync() => Task.CompletedTask;
// }
//}
@@ -13,11 +13,12 @@ using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestSocketClient: BaseSocketClient
internal class TestSocketClient: BaseSocketClient
{
public TestSubSocketClient SubClient { get; }
@@ -41,12 +42,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
public TestSocket CreateSocket()
{
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
}
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestSubSocketClient : SocketApiClient
{
private MessagePath _channelPath = MessagePath.Get().Property("channel");
private MessagePath _actionPath = MessagePath.Get().Property("action");
private MessagePath _topicPath = MessagePath.Get().Property("topic");
public Subscription TestSubscription { get; private set; } = null;
@@ -85,7 +87,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
internal IWebsocket CreateSocketInternal(string address)
{
@@ -110,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var id = message.GetValue<string>(_channelPath);
id ??= message.GetValue<string>(_topicPath);
return id;
return message.GetValue<string>(_actionPath) + "-" + id;
}
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
+8 -1
View File
@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -35,6 +37,10 @@ Global
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -42,6 +48,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Security;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.MessageParsing;
@@ -9,17 +8,17 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Api credentials, used to sign requests accessing private endpoints
/// </summary>
public class ApiCredentials: IDisposable
public class ApiCredentials
{
/// <summary>
/// The api key to authenticate requests
/// The api key / label to authenticate requests
/// </summary>
public SecureString? Key { get; }
public string Key { get; }
/// <summary>
/// The api secret to authenticate requests
/// The api secret or private key to authenticate requests
/// </summary>
public SecureString? Secret { get; }
public string Secret { get; }
/// <summary>
/// Type of the credentials
@@ -29,21 +28,21 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
{
if (key == null || secret == null)
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
@@ -51,39 +50,13 @@ namespace CryptoExchange.Net.Authentication
Secret = secret;
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
Key = key.ToSecureString();
Secret = secret.ToSecureString();
}
/// <summary>
/// Copy the credentials
/// </summary>
/// <returns></returns>
public virtual ApiCredentials Copy()
{
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
return new ApiCredentials(Key, Secret, CredentialType);
}
/// <summary>
@@ -103,19 +76,10 @@ namespace CryptoExchange.Net.Authentication
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key.ToSecureString();
Secret = secret.ToSecureString();
Key = key;
Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin);
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Secret?.Dispose();
}
}
}
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Base class for authentication providers
/// </summary>
public abstract class AuthenticationProvider : IDisposable
public abstract class AuthenticationProvider
{
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
protected byte[] _sBytes;
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key;
/// <summary>
/// ctor
/// </summary>
@@ -38,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
}
/// <summary>
@@ -49,18 +54,18 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method">The method of the request</param>
/// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
/// <param name="requestBodyFormat">The formatting of the request body</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
public abstract void AuthenticateRequest(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
IDictionary<string, object> uriParameters,
IDictionary<string, object> bodyParameters,
Dictionary<string, string> headers,
ref IDictionary<string, object>? uriParameters,
ref IDictionary<string, object>? bodyParameters,
ref Dictionary<string, string>? headers,
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
@@ -366,7 +371,7 @@ namespace CryptoExchange.Net.Authentication
{
#if NETSTANDARD2_1_OR_GREATER
// Read from pem private key
var key = _credentials.Secret!.GetString()
var key = _credentials.Secret!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
@@ -381,7 +386,7 @@ namespace CryptoExchange.Net.Authentication
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
{
// Read from xml private key format
rsa.FromXmlString(_credentials.Secret!.GetString());
rsa.FromXmlString(_credentials.Secret!);
}
else
{
@@ -434,10 +439,18 @@ namespace CryptoExchange.Net.Authentication
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
}
/// <inheritdoc />
public void Dispose()
/// <summary>
/// Return the serialized request body
/// </summary>
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
{
_credentials?.Dispose();
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]);
else
return serializer.Serialize(parameters);
}
}
+54
View File
@@ -0,0 +1,54 @@
using System;
using System.Collections.Concurrent;
namespace CryptoExchange.Net.Caching
{
internal class MemoryCache
{
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="value">Cache value</param>
public void Add(string key, object value)
{
var cacheItem = new CacheItem(DateTime.UtcNow, value);
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
}
/// <summary>
/// Get a cached value
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="maxAge">The max age of the cached entry</param>
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
_cache.TryGetValue(key, out CacheItem value);
if (value == null)
return null;
if (DateTime.UtcNow - value.CacheTime > maxAge)
{
_cache.TryRemove(key, out _);
return null;
}
return value.Value;
}
private class CacheItem
{
public DateTime CacheTime { get; }
public object Value { get; }
public CacheItem(DateTime cacheTime, object value)
{
CacheTime = cacheTime;
Value = value;
}
}
}
}
+9 -8
View File
@@ -1,7 +1,9 @@
using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients
@@ -36,6 +38,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public bool OutputOriginalData { get; }
/// <summary>
/// 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;
/// <summary>
/// Api options
/// </summary>
@@ -65,10 +72,7 @@ namespace CryptoExchange.Net.Clients
BaseAddress = baseAddress;
if (apiCredentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
}
}
/// <summary>
@@ -79,16 +83,14 @@ namespace CryptoExchange.Net.Clients
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset);
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
ApiOptions.ApiCredentials = credentials;
if (credentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
}
}
/// <summary>
@@ -97,7 +99,6 @@ namespace CryptoExchange.Net.Clients
public virtual void Dispose()
{
_disposing = true;
AuthenticationProvider?.Dispose();
}
}
}
+26 -1
View File
@@ -12,6 +12,28 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public abstract class BaseClient : IDisposable
{
/// <summary>
/// Version of the CryptoExchange.Net base library
/// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version;
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{
get
{
lock(_versionLock)
{
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version;
return _exchangeVersion;
}
}
}
/// <summary>
/// The name of the API the client is for
/// </summary>
@@ -27,6 +49,9 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal ILogger _logger;
private object _versionLock = new object();
private Version _exchangeVersion;
/// <summary>
/// Provided client options
/// </summary>
@@ -57,7 +82,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}");
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
+168 -71
View File
@@ -8,6 +8,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.JsonNet;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
@@ -75,7 +76,8 @@ namespace CryptoExchange.Net.Clients
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
{ HttpMethod.Put, HttpMethodParameterPosition.InBody },
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
};
/// <inheritdoc />
@@ -84,6 +86,10 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
/// <summary>
/// Memory cache
/// </summary>
private static MemoryCache _cache = new MemoryCache();
/// <summary>
/// ctor
@@ -149,7 +155,7 @@ namespace CryptoExchange.Net.Clients
/// <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>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
@@ -157,16 +163,72 @@ namespace CryptoExchange.Net.Clients
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
baseAddress,
definition,
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken,
additionalHeaders,
weight);
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
/// <param name="cancellationToken">Cancellation token</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>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
{
string? cacheKey = null;
if (ShouldCache(definition))
{
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
_logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null)
{
_logger.CacheHit(cacheKey);
var original = (WebCallResult<T>)cachedValue;
return original.Cached();
}
_logger.CacheNotHit(cacheKey);
}
int currentTry = 0;
while (true)
{
currentTry++;
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!);
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
var request = CreateRequest(
requestId,
baseAddress,
definition,
uriParameters,
bodyParameters,
additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (!result)
@@ -177,6 +239,12 @@ namespace CryptoExchange.Net.Clients
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
continue;
if (result.Success &&
ShouldCache(definition))
{
_cache.Add(cacheKey!, result);
}
return result;
}
}
@@ -184,23 +252,22 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Prepare before sending a request. Sync time between client and server and check rate limits
/// </summary>
/// <param name="requestId">Request id</param>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync(
int requestId,
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
{
var requestId = ExchangeHelpers.NextId();
var requestWeight = weight ?? definition.Weight;
// Time sync
@@ -242,14 +309,14 @@ namespace CryptoExchange.Net.Clients
}
// Endpoint specific rate limiting
if (definition.EndpointLimitCount != null && definition.EndpointLimitPeriod != null)
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
{
if (definition.RateLimitGate == null)
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
}
@@ -261,27 +328,30 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Creates a request object
/// </summary>
/// <param name="requestId">Id of the request</param>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="uriParameters">The query parameters of the request</param>
/// <param name="bodyParameters">The body parameters of the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns>
protected virtual IRequest CreateRequest(
int requestId,
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders)
{
parameters ??= new ParameterCollection();
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
var uri = new Uri(baseAddress.AppendPath(definition.Path));
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var requestId = ExchangeHelpers.NextId();
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
Dictionary<string, string>? headers = null;
if (AuthenticationProvider != null)
{
try
@@ -290,13 +360,14 @@ namespace CryptoExchange.Net.Clients
this,
uri,
definition.Method,
uriParameters,
bodyParameters,
headers,
ref uriParams,
ref bodyParams,
ref headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat);
bodyFormat
);
}
catch (Exception ex)
{
@@ -304,24 +375,18 @@ namespace CryptoExchange.Net.Clients
}
}
// Sanity check
foreach (var param in parameters)
{
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
{
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
$"should return provided parameters in either the uri or body parameters output");
}
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
uri = uri.SetParameters(uriParameters, arraySerialization);
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
if (uriParams != null)
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
if (headers != null)
{
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null)
{
@@ -342,8 +407,8 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Count != 0)
WriteParamBody(request, bodyParameters, contentType);
if (bodyParams != null && bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
@@ -416,6 +481,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="requestWeight">Credits used for the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <param name="gate">The ratelimit gate to use</param>
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
@@ -429,9 +495,25 @@ namespace CryptoExchange.Net.Clients
ArrayParametersSerialization? arraySerialization = null,
int requestWeight = 1,
Dictionary<string, string>? additionalHeaders = null,
IRateLimitGate? gate = null
IRateLimitGate? gate = null,
bool preventCaching = false
) where T : class
{
var key = uri.ToString() + method + signed + parameters?.ToFormData();
if (ShouldCache(method) && !preventCaching)
{
_logger.CheckingCache(key);
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
if (cachedValue != null)
{
_logger.CacheHit(key);
var original = (WebCallResult<T>)cachedValue;
return original.Cached();
}
_logger.CacheNotHit(key);
}
int currentTry = 0;
while (true)
{
@@ -449,6 +531,13 @@ namespace CryptoExchange.Net.Clients
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
continue;
if (result.Success &&
ShouldCache(method) &&
!preventCaching)
{
_cache.Add(key, result);
}
return result;
}
}
@@ -588,47 +677,47 @@ namespace CryptoExchange.Net.Clients
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
}
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (!valid)
{
// Invalid json
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
// Json response received
var parsedError = TryParseError(accessor);
if (parsedError != null)
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var exceptionInfo = requestException.ToLogString();
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
}
else
{
// Request timed out
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
}
}
finally
@@ -722,8 +811,8 @@ namespace CryptoExchange.Net.Clients
}
var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null;
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null;
if (AuthenticationProvider != null)
{
try
@@ -732,13 +821,14 @@ namespace CryptoExchange.Net.Clients
this,
uri,
method,
uriParameters,
bodyParameters,
headers,
ref uriParameters,
ref bodyParameters,
ref headers,
signed,
arraySerialization,
parameterPosition,
bodyFormat);
bodyFormat
);
}
catch (Exception ex)
{
@@ -746,24 +836,18 @@ namespace CryptoExchange.Net.Clients
}
}
// Sanity check
foreach (var param in parameters)
{
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
{
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
$"should return provided parameters in either the uri or body parameters output");
}
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
uri = uri.SetParameters(uriParameters, arraySerialization);
if (uriParameters != null)
uri = uri.SetParameters(uriParameters, arraySerialization);
var request = RequestFactory.Create(method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
if (headers != null)
{
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null)
{
@@ -784,7 +868,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Any())
if (bodyParameters?.Any() == true)
WriteParamBody(request, bodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
@@ -804,7 +888,11 @@ namespace CryptoExchange.Net.Clients
if (contentType == Constants.JsonContentHeader)
{
// Write the parameters as json in the body
var stringData = CreateSerializer().Serialize(parameters);
string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
else
stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType);
}
else if (contentType == Constants.FormContentHeader)
@@ -877,14 +965,14 @@ namespace CryptoExchange.Net.Clients
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
var localTime = DateTime.UtcNow;
@@ -913,7 +1001,16 @@ namespace CryptoExchange.Net.Clients
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
private bool ShouldCache(RequestDefinition definition)
=> ClientOptions.CachingEnabled
&& definition.Method == HttpMethod.Get
&& !definition.PreventCaching;
private bool ShouldCache(HttpMethod method)
=> ClientOptions.CachingEnabled
&& method == HttpMethod.Get;
}
}
+145 -43
View File
@@ -52,11 +52,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal bool UnhandledMessageExpected { get; set; }
/// <summary>
/// If true a subscription will accept message before the confirmation of a subscription has been received
/// </summary>
protected bool HandleMessageBeforeConfirmation { get; set; }
/// <summary>
/// The rate limiters
/// </summary>
@@ -72,6 +67,16 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
/// <summary>
/// List of address to keep an alive connection to
/// </summary>
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
/// <summary>
/// Whether to allow multiple subscriptions with the same topic on the same connection
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <inheritdoc />
public double IncomingKbps
{
@@ -136,6 +141,16 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
/// <summary>
/// Keep an open connection to this url
/// </summary>
/// <param name="url"></param>
/// <param name="auth"></param>
protected virtual void SetDedicatedConnection(string url, bool auth)
{
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
}
/// <summary>
/// Add a query to periodically send on each connection
/// </summary>
@@ -178,7 +193,10 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null)
{
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
}
SocketConnection socketConnection;
var released = false;
@@ -198,12 +216,11 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
@@ -241,7 +258,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
}
var waitEvent = new ManualResetEvent(false);
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection);
if (subQuery != null)
{
@@ -250,11 +267,18 @@ namespace CryptoExchange.Net.Clients
if (!subResult)
{
waitEvent?.Set();
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
var unsubscribe = subResult.Error is CancellationRequestedError;
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Confirmed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
else
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
subscription.HandleSubQueryResponse(subQuery.Response!);
@@ -278,34 +302,41 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary>
/// <typeparam name="T">Expected result type</typeparam>
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query)
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
{
return QueryAsync(BaseAddress, query);
return QueryAsync(BaseAddress, query, ct);
}
/// <summary>
/// Send a query on a socket connection and wait for the response
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="url">The url for the request</param>
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query)
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
{
if (_disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
SocketConnection socketConnection;
var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<T>(default);
return socketResult.As<THandlerResponse>(default);
socketConnection = socketResult.Data;
@@ -318,7 +349,7 @@ namespace CryptoExchange.Net.Clients
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(connectResult.Error!);
return new CallResult<THandlerResponse>(connectResult.Error!);
}
finally
{
@@ -329,10 +360,13 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<T>(new ServerError("Socket is paused"));
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
}
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false);
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
}
/// <summary>
@@ -356,7 +390,11 @@ namespace CryptoExchange.Net.Clients
if (!authenticated || socket.Authenticated)
return new CallResult(null);
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
await socket.CloseAsync().ConfigureAwait(false);
return result;
}
/// <summary>
@@ -370,7 +408,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest();
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@@ -395,7 +433,7 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
@@ -444,19 +482,36 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
{
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
if (result != null)
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected);
SocketConnection connection;
if (!dedicatedRequestConnection)
{
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
}
else
{
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
}
if (connection != null)
{
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
return new CallResult<SocketConnection>(result);
return new CallResult<SocketConnection>(connection);
}
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
@@ -473,6 +528,15 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
{
IsDedicatedRequestConnection = dedicatedRequestConnection,
Authenticated = authenticated
};
}
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
@@ -491,6 +555,19 @@ namespace CryptoExchange.Net.Clients
{
}
/// <summary>
/// Process connect rate limited
/// </summary>
protected async virtual Task HandleConnectRateLimitedAsync()
{
if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null)
{
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
_logger.AddingRetryAfterGuard(retryAfter);
await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false);
}
}
/// <summary>
/// Connect a socket
/// </summary>
@@ -515,7 +592,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address to connect to</param>
/// <returns></returns>
protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new(new Uri(address), ClientOptions.AutoReconnect)
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
{
KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval,
@@ -592,8 +669,8 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection))
tasks.Add(connection.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
@@ -616,6 +693,23 @@ namespace CryptoExchange.Net.Clients
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <inheritdoc />
public virtual async Task<CallResult> PrepareConnectionsAsync()
{
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
if (!socketResult)
return socketResult.AsDataless();
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult(connectResult.Error!);
}
return new CallResult(null);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
@@ -699,11 +793,18 @@ namespace CryptoExchange.Net.Clients
public override void Dispose()
{
_disposing = true;
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
var tasks = new List<Task>();
{
_logger.DisposingSocketClient();
_ = UnsubscribeAllAsync();
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
foreach (var connection in socketList)
{
tasks.Add(connection.CloseAsync());
}
}
semaphoreSlim?.Dispose();
base.Dispose();
}
@@ -718,9 +819,10 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Preprocess a stream message
/// </summary>
/// <param name="connection"></param>
/// <param name="type"></param>
/// <param name="data"></param>
/// <returns></returns>
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
}
}
@@ -0,0 +1,62 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters.JsonNet
{
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
if (Nullable.GetUnderlyingType(objectType) != null)
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
return objectType == typeof(decimal);
}
/// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer)
{
try
{
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch (OverflowException)
{
// Value doesn't fit decimal; set it to max value
return decimal.MaxValue;
}
}
if (reader.TokenType == JsonToken.String)
{
try
{
var value = reader.Value!.ToString();
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch (OverflowException)
{
// Value doesn't fit decimal; set it to max value
return decimal.MaxValue;
}
}
return null;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue(value);
}
}
}
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -32,17 +32,73 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public Type? JsonConverterType { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
}
private class ArrayConverterInner<T> : JsonConverter<T>
{
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// TODO
throw new NotImplementedException();
if (value == null)
{
writer.WriteNullValue();
return;
}
writer.WriteStartArray();
var valueType = value.GetType();
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
typeAttributes = CacheTypeAttributes(valueType);
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
{
writer.WriteNullValue();
last += 1;
}
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null)
{
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType);
typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter);
}
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.PropertyInfo.PropertyType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
writer.WriteEndArray();
}
/// <inheritdoc />
@@ -52,7 +108,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default;
var result = Activator.CreateInstance(typeToConvert);
return (T)ParseObject(ref reader, result, typeToConvert);
return (T)ParseObject(ref reader, result, typeToConvert, options);
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
@@ -70,7 +139,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
});
}
@@ -78,10 +148,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return attributes;
}
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("1");
throw new Exception("Not an array");
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
attributes = CacheTypeAttributes(objectType);
@@ -92,37 +162,58 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray)
break;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
var targetType = attribute.PropertyInfo.PropertyType;
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{
index++;
continue;
}
object? value = null;
if (attribute.JsonConverterType != null)
foreach (var attribute in indexAttributes)
{
// Has JsonConverter attribute
var options = new JsonSerializerOptions();
options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType));
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType);
}
else
{
value = reader.TokenType switch
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverterType != null)
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
{
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType);
newOptions = new JsonSerializerOptions
{
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
Converters = { converter },
};
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
}
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, newOptions);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value == null ? null : value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, attribute.PropertyInfo.PropertyType, CultureInfo.InvariantCulture));
index++;
}
@@ -0,0 +1,46 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
try
{
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
}
try
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
}
}
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Primitives;
using System;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -58,6 +57,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1"
|| stringValue == "0001-01-01T00:00:00Z"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{
return default;
@@ -154,6 +154,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
// Parse 1637745563.000 format
if (doubleValue <= 0)
return default;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
@@ -19,13 +19,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
return null;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
try
{
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
}
return reader.GetDecimal();
try
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
}
/// <inheritdoc />
@@ -211,5 +211,37 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
}
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <typeparam name="T">Enum type</typeparam>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString<T>(string value) where T : Enum
{
var type = typeof(T);
if (!_mapping.TryGetValue(type, out var enumMapping))
enumMapping = AddMapping(type);
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>)))
{
return (T)mapping.Key;
}
try
{
// If no explicit mapping is found try to parse string
return (T)Enum.Parse(type, value, true);
}
catch (Exception)
{
return default;
}
}
}
}
@@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Int converter
/// </summary>
public class IntConverter : JsonConverter<int?>
{
/// <inheritdoc />
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
return reader.GetInt32();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
}
@@ -0,0 +1,40 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Int converter
/// </summary>
public class LongConverter : JsonConverter<long?>
{
/// <inheritdoc />
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return null;
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
return reader.GetInt64();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
}
@@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Read string or number as string
/// </summary>
public class NumberStringConverter : JsonConverter<string?>
{
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt64(out var value))
return value.ToString();
return reader.GetDecimal().ToString();
}
try
{
return reader.GetString();
}
catch (Exception)
{
return null;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Text.Json.Serialization;
using System.Text.Json;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectStringConverter<T> : JsonConverter<T>
{
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var value = reader.GetString();
if (string.IsNullOrEmpty(value))
return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
if (value is null)
writer.WriteStringValue("");
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
}
}
}
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
new EnumConverter(),
new BoolConverter(),
new DecimalConverter(),
new IntConverter(),
new LongConverter()
}
};
}
@@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var info = $"Deserialize unknown Exception: {ex.Message}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
@@ -68,6 +73,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var info = $"Unknown exception: {ex.Message}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
@@ -116,13 +126,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_serializerOptions);
}
catch { }
return default;
}
if (typeof(T) == typeof(string))
{
if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path) => throw new NotImplementedException();
public List<T?>? GetValues<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<List<T>>()!;
}
private JsonElement? GetPathNode(MessagePath path)
{
@@ -237,6 +273,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
_stream?.Dispose();
_stream = null;
_document?.Dispose();
_document = null;
}
@@ -256,6 +293,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try
{
var firstByte = data.Span[0];
if (firstByte != 0x7b && firstByte != 0x5b)
{
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsJson = false;
return new CallResult(new ServerError("Not a json value"));
}
_document = JsonDocument.Parse(data);
IsJson = true;
return new CallResult(null);
@@ -284,6 +329,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Clear()
{
_bytes = null;
_document?.Dispose();
_document = null;
}
}
+8 -8
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>7.5.1</PackageVersion>
<AssemblyVersion>7.5.1</AssemblyVersion>
<FileVersion>7.5.1</FileVersion>
<PackageVersion>8.2.0</PackageVersion>
<AssemblyVersion>8.2.0</AssemblyVersion>
<FileVersion>8.2.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -35,7 +35,7 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -52,12 +52,12 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>
+103 -21
View File
@@ -1,6 +1,11 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net
{
@@ -11,14 +16,26 @@ namespace CryptoExchange.Net
{
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
{
{ 1, "F" },
{ 2, "G" },
{ 3, "H" },
{ 4, "J" },
{ 5, "K" },
{ 6, "M" },
{ 7, "N" },
{ 8, "Q" },
{ 9, "U" },
{ 10, "V" },
{ 11, "X" },
{ 12, "Z" },
};
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// </summary>
private static int _lastId;
/// <summary>
/// Lock for id generating
/// </summary>
private static object _idLock = new();
/// <summary>
/// Clamp a value between a min and max
@@ -58,6 +75,11 @@ namespace CryptoExchange.Net
{
value -= offset;
}
else if(roundingType == RoundingType.Up)
{
if (offset != 0)
value += (step.Value - offset);
}
else
{
if (offset < step / 2)
@@ -110,17 +132,23 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Rounds a value down to
/// Rounds a value down
/// </summary>
/// <param name="i"></param>
/// <param name="decimalPlaces"></param>
/// <returns></returns>
public static decimal RoundDown(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power;
}
/// <summary>
/// Rounds a value up
/// </summary>
public static decimal RoundUp(decimal i, double decimalPlaces)
{
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Ceiling(i * power) / power;
}
/// <summary>
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
/// </summary>
@@ -135,24 +163,13 @@ namespace CryptoExchange.Net
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
/// </summary>
/// <returns></returns>
public static int NextId()
{
lock (_idLock)
{
_lastId += 1;
return _lastId;
}
}
public static int NextId() => Interlocked.Increment(ref _lastId);
/// <summary>
/// Return the last unique id that was generated
/// </summary>
/// <returns></returns>
public static int LastId()
{
lock (_idLock)
return _lastId;
}
public static int LastId() => _lastId;
/// <summary>
/// Generate a random string of specified length
@@ -191,5 +208,70 @@ namespace CryptoExchange.Net
return source + RandomString(totalLength - source.Length);
}
/// <summary>
/// Get the month representation for futures symbol based on the delivery month
/// </summary>
/// <param name="time">Delivery time</param>
/// <returns></returns>
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
/// <summary>
/// Execute multiple requests to retrieve multiple pages of the result set
/// </summary>
/// <typeparam name="T">Type of the client</typeparam>
/// <typeparam name="U">Type of the request</typeparam>
/// <param name="paginatedFunc">The func to execute with each request</param>
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<IEnumerable<T>>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<IEnumerable<T>>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<T>();
ExchangeWebResult<IEnumerable<T>> batch;
INextPageToken? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch;
if (!batch || ct.IsCancellationRequested)
break;
result.AddRange(batch.Data);
nextPageToken = batch.NextPageToken;
if (nextPageToken == null)
break;
}
}
/// <summary>
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary>
/// <param name="symbol">The symbol as retrieved from the exchange</param>
/// <param name="quantity">Quantity to trade</param>
/// <param name="price">Price to trade at</param>
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
{
adjustedPrice = price;
adjustedQuantity = quantity;
var minNotionalAdjust = false;
if (price != null)
{
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
{
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
minNotionalAdjust = true;
}
}
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
}
}
}
+143 -108
View File
@@ -4,11 +4,12 @@ using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Web;
using CryptoExchange.Net.Objects;
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net
{
@@ -96,6 +97,9 @@ namespace CryptoExchange.Net
var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters)
{
if (kvp.Value is null)
continue;
if (kvp.Value.GetType().IsArray)
{
var array = (Array)kvp.Value;
@@ -110,92 +114,6 @@ namespace CryptoExchange.Net
return formData.ToString();
}
/// <summary>
/// Get the string the secure string is representing
/// </summary>
/// <param name="source">The source secure string</param>
/// <returns></returns>
public static string GetString(this SecureString source)
{
lock (source)
{
string result;
var length = source.Length;
var pointer = IntPtr.Zero;
var chars = new char[length];
try
{
pointer = Marshal.SecureStringToBSTR(source);
Marshal.Copy(pointer, chars, 0, length);
result = string.Join("", chars);
}
finally
{
if (pointer != IntPtr.Zero)
{
Marshal.ZeroFreeBSTR(pointer);
}
}
return result;
}
}
/// <summary>
/// Are 2 secure strings equal
/// </summary>
/// <param name="ss1">Source secure string</param>
/// <param name="ss2">Compare secure string</param>
/// <returns>True if equal by value</returns>
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
{
IntPtr bstr1 = IntPtr.Zero;
IntPtr bstr2 = IntPtr.Zero;
try
{
bstr1 = Marshal.SecureStringToBSTR(ss1);
bstr2 = Marshal.SecureStringToBSTR(ss2);
int length1 = Marshal.ReadInt32(bstr1, -4);
int length2 = Marshal.ReadInt32(bstr2, -4);
if (length1 == length2)
{
for (int x = 0; x < length1; ++x)
{
byte b1 = Marshal.ReadByte(bstr1, x);
byte b2 = Marshal.ReadByte(bstr2, x);
if (b1 != b2) return false;
}
}
else
{
return false;
}
return true;
}
finally
{
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
}
}
/// <summary>
/// Create a secure string from a string
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static SecureString ToSecureString(this string source)
{
var secureString = new SecureString();
foreach (var c in source)
secureString.AppendChar(c);
secureString.MakeReadOnly();
return secureString;
}
/// <summary>
/// Validates an int is one of the allowed values
/// </summary>
@@ -271,6 +189,16 @@ namespace CryptoExchange.Net
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
}
/// <summary>
/// Format a string to RFC3339/ISO8601 string
/// </summary>
/// <param name="dateTime"></param>
/// <returns></returns>
public static string ToRfc3339String(this DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
}
/// <summary>
/// Format an exception and inner exception to a readable string
/// </summary>
@@ -315,26 +243,6 @@ namespace CryptoExchange.Net
return url.TrimEnd('/');
}
/// <summary>
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
/// </summary>
/// <param name="path">The total path string</param>
/// <param name="values">The values to fill</param>
/// <returns></returns>
public static string FillPathParameters(this string path, params string[] values)
{
foreach (var value in values)
{
var index = path.IndexOf("{}", StringComparison.Ordinal);
if (index >= 0)
{
path = path.Remove(index, 2);
path = path.Insert(index, value);
}
}
return path;
}
/// <summary>
/// Create a new uri with the provided parameters as query
/// </summary>
@@ -450,7 +358,7 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Decompress using Gzip
/// Decompress using GzipStream
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
@@ -464,6 +372,133 @@ namespace CryptoExchange.Net
deflateStream.CopyTo(decompressedStream);
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
}
/// <summary>
/// Decompress using DeflateStream
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
{
var output = new MemoryStream();
using (var compressStream = new MemoryStream(input.ToArray()))
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
decompressor.CopyTo(output);
output.Position = 0;
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
}
/// <summary>
/// Whether the trading mode is linear
/// </summary>
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
/// <summary>
/// Whether the trading mode is inverse
/// </summary>
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
/// <summary>
/// Whether the trading mode is perpetual
/// </summary>
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
/// <summary>
/// Whether the trading mode is delivery
/// </summary>
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
/// <summary>
/// Register rest client interfaces
/// </summary>
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
{
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IAssetsRestClient)client(x)!);
if (typeof(IBalanceRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBalanceRestClient)client(x)!);
if (typeof(IDepositRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IDepositRestClient)client(x)!);
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineRestClient)client(x)!);
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IRecentTradeRestClient)client(x)!);
if (typeof(ITradeHistoryRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITradeHistoryRestClient)client(x)!);
if (typeof(IWithdrawalRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
if (typeof(IFuturesOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderRestClient)client(x)!);
if (typeof(IFuturesSymbolRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesSymbolRestClient)client(x)!);
if (typeof(IFuturesTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesTickerRestClient)client(x)!);
if (typeof(IIndexPriceKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IIndexPriceKlineRestClient)client(x)!);
if (typeof(ILeverageRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ILeverageRestClient)client(x)!);
if (typeof(IMarkPriceKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IMarkPriceKlineRestClient)client(x)!);
if (typeof(IOpenInterestRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOpenInterestRestClient)client(x)!);
if (typeof(IPositionHistoryRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
return services;
}
/// <summary>
/// Register socket client interfaces
/// </summary>
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
{
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineSocketClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickerSocketClient)client(x)!);
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickersSocketClient)client(x)!);
if (typeof(ITradeSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITradeSocketClient)client(x)!);
if (typeof(IUserTradeSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IUserTradeSocketClient)client(x)!);
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionSocketClient)client(x)!);
return services;
}
}
}
@@ -8,131 +8,87 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common rest client endpoints
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
public interface IBaseRestClient
{
/// <summary>
/// The name of the exchange
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
string ExchangeName { get; }
/// <summary>
/// Should be triggered on order placing
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
event Action<OrderId> OnOrderPlaced;
/// <summary>
/// Should be triggered on order cancelling
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
event Action<OrderId> OnOrderCanceled;
/// <summary>
/// Get the symbol name based on a base and quote asset
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <returns></returns>
string GetSymbolName(string baseAsset, string quoteAsset);
/// <summary>
/// Get a list of symbols for the exchange
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
/// <summary>
/// Get a ticker for the exchange
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">The symbol to get klines for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// Get a list of tickers for the exchange
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
/// <summary>
/// Get a list of candles for a given symbol on the exchange
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">The symbol to retrieve the candles for</param>
/// <param name="timespan">The timespan to retrieve the candles for. The supported value are dependent on the exchange</param>
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
/// <param name="limit">[Optional] Max number of results</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
/// <summary>
/// Get the order book for a symbol
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">The symbol to get the book for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// The recent trades for a symbol
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">The symbol to get the trades for</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
/// <summary>
/// Get balances
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
/// <summary>
/// Get an order by id
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Get trades for an order by id
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Get a list of open orders
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Get a list of closed orders
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <summary>
/// Cancel an order by id
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="orderId">The id</param>
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
}
}
@@ -7,30 +7,18 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common futures endpoints
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
public interface IFuturesClient : IBaseRestClient
{
/// <summary>
/// Place an order
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">The symbol the order is for</param>
/// <param name="side">The side of the order</param>
/// <param name="type">The type of the order</param>
/// <param name="quantity">The quantity of the order</param>
/// <param name="price">The price of the order, only for limit orders</param>
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
/// <param name="leverage">[Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand)</param>
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
/// <summary>
/// Get position
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
}
}
@@ -6,22 +6,13 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients
{
/// <summary>
/// Common spot endpoints
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="" /> for more info.
/// </summary>
public interface ISpotClient: IBaseRestClient
{
/// <summary>
/// Place an order
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
/// </summary>
/// <param name="symbol">The symbol the order is for</param>
/// <param name="side">The side of the order</param>
/// <param name="type">The type of the order</param>
/// <param name="quantity">The quantity of the order</param>
/// <param name="price">The price of the order, only for limit orders</param>
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns>The id of the resulting order</returns>
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
}
}
@@ -1,4 +1,7 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces
{
@@ -17,8 +20,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <param name="tradingMode">The trading mode</param>
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
/// <returns></returns>
string FormatSymbol(string baseAsset, string quoteAsset);
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set the API credentials for this API client
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
@@ -16,10 +17,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public int Id { get; }
/// <summary>
/// Whether this listener can handle data
/// </summary>
public bool CanHandleData { get; }
/// <summary>
/// The identifiers for this processor
/// </summary>
public HashSet<string> ListenerIdentifiers { get; }
@@ -29,7 +26,7 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
CallResult Handle(SocketConnection connection, DataEvent<object> message);
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces
@@ -23,5 +24,12 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="options">Options for the order book</param>
/// <returns></returns>
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null);
/// <summary>
/// Create a new order book by base and quote asset names
/// </summary>
/// <param name="symbol">Symbol</param>
/// <param name="options">Options for the order book</param>
/// <returns></returns>
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null);
}
}
@@ -1,7 +1,6 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
@@ -24,6 +23,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestWeight">The weight of the request</param>
/// <param name="ct">Cancellation token to cancel waiting</param>
/// <returns>The time in milliseconds spend waiting</returns>
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, string? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
}
}
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using System.Threading.Tasks;
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Prepare connections which can subsequently be used for sending websocket requests. Note that this is not required. If not prepared it will be initialized at the first websocket request.
/// </summary>
/// <returns></returns>
Task<CallResult> PrepareConnectionsAsync();
}
}
+6 -2
View File
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Websocket message received event
/// </summary>
event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
/// <summary>
/// Websocket sent event, RequestId as parameter
/// </summary>
@@ -27,6 +27,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
event Func<int, Task>? OnRequestRateLimited;
/// <summary>
/// Connection was ratelimited and couldn't be established
/// </summary>
event Func<Task>? OnConnectRateLimited;
/// <summary>
/// Websocket error event
/// </summary>
event Func<Exception, Task> OnError;
@@ -78,7 +82,7 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="id"></param>
/// <param name="data"></param>
/// <param name="weight"></param>
void Send(int id, string data, int weight);
bool Send(int id, string data, int weight);
/// <summary>
/// Reconnect the socket
/// </summary>
@@ -3,7 +3,8 @@ using System;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class CryptoExchangeWebSocketClientLoggingExtension
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class CryptoExchangeWebSocketClientLoggingExtension
{
private static readonly Action<ILogger, int, Exception?> _connecting;
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
@@ -24,6 +25,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseConfirmation;
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
@@ -32,6 +34,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
static CryptoExchangeWebSocketClientLoggingExtension()
{
@@ -151,7 +154,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
_receiveLoopStoppedWithException = LoggerMessage.Define<int>(
LogLevel.Warning,
LogLevel.Error,
new EventId(1024, "ReceiveLoopStoppedWithException"),
"[Sckt {SocketId}] receive loop stopped with exception");
@@ -169,6 +172,17 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
_receivedCloseConfirmation = LoggerMessage.Define<int, string, string>(
LogLevel.Debug,
new EventId(1028, "ReceivedCloseMessage"),
"[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(1028, "SocketProcessingStateChanged"),
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
}
public static void SocketConnecting(
@@ -285,6 +299,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedCloseConfirmation(
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
{
_receivedCloseConfirmation(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedPartialMessage(
this ILogger logger, int socketId, int countBytes)
{
@@ -332,5 +352,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
}
public static void SocketProcessingStateChanged(
this ILogger logger, int socketId, string prevState, string newState)
{
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
}
}
}
@@ -3,7 +3,8 @@ using System;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class RateLimitGateLoggingExtensions
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class RateLimitGateLoggingExtensions
{
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
@@ -6,7 +6,8 @@ using System.Net.Http;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class RestApiClientLoggingExtensions
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class RestApiClientLoggingExtensions
{
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
@@ -17,6 +18,9 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
static RestApiClientLoggingExtensions()
@@ -65,6 +69,21 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(4008, "RestApiSendRequest"),
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
_restApiCheckingCache = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4009, "RestApiCheckingCache"),
"Checking cache for key {Key}");
_restApiCacheHit = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4010, "RestApiCacheHit"),
"Cache hit for key {Key}");
_restApiCacheNotHit = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4011, "RestApiCacheNotHit"),
"Cache not hit for key {Key}");
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
@@ -111,5 +130,20 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
}
public static void CheckingCache(this ILogger logger, string key)
{
_restApiCheckingCache(logger, key, null);
}
public static void CacheHit(this ILogger logger, string key)
{
_restApiCacheHit(logger, key, null);
}
public static void CacheNotHit(this ILogger logger, string key)
{
_restApiCacheNotHit(logger, key, null);
}
}
}
@@ -3,7 +3,8 @@ using System;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class SocketApiClientLoggingExtension
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class SocketApiClientLoggingExtension
{
private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection;
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment;
@@ -21,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, Exception?> _disposingSocketClient;
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
static SocketApiClientLoggingExtension()
{
@@ -103,6 +105,11 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections");
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -184,5 +191,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_reconnectingAllConnections(logger, connectionCount, null);
}
public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter)
{
_addingRetryAfterGuard(logger, retryAfter, null);
}
}
}
@@ -4,7 +4,8 @@ using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class SocketConnectionLoggingExtension
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class SocketConnectionLoggingExtension
{
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
@@ -4,7 +4,9 @@ using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
internal static class SymbolOrderBookLoggingExtensions
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class SymbolOrderBookLoggingExtensions
{
private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged;
private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting;
@@ -0,0 +1,292 @@
using System;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class TrackerLoggingExtensions
{
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _klineTrackerStatusChanged;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarting;
private static readonly Action<ILogger, string, string, Exception?> _klineTrackerStartFailed;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarted;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopping;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopped;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerInitialDataSet;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineUpdated;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineAdded;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionLost;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionClosed;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionRestored;
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _tradeTrackerStatusChanged;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarting;
private static readonly Action<ILogger, string, string, Exception?> _tradeTrackerStartFailed;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarted;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopping;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopped;
private static readonly Action<ILogger, string, int, long, Exception?> _tradeTrackerInitialDataSet;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotSkip;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotApplied;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerTradeAdded;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionLost;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionClosed;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionRestored;
static TrackerLoggingExtensions()
{
_klineTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug,
new EventId(6001, "KlineTrackerStatusChanged"),
"Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
_klineTrackerStarting = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6002, "KlineTrackerStarting"),
"Kline tracker for {Symbol} starting");
_klineTrackerStartFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(6003, "KlineTrackerStartFailed"),
"Kline tracker for {Symbol} failed to start: {Error}");
_klineTrackerStarted = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6004, "KlineTrackerStarted"),
"Kline tracker for {Symbol} started");
_klineTrackerStopping = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6005, "KlineTrackerStopping"),
"Kline tracker for {Symbol} stopping");
_klineTrackerStopped = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6006, "KlineTrackerStopped"),
"Kline tracker for {Symbol} stopped");
_klineTrackerInitialDataSet = LoggerMessage.Define<string, DateTime>(
LogLevel.Debug,
new EventId(6007, "KlineTrackerInitialDataSet"),
"Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}");
_klineTrackerKlineUpdated = LoggerMessage.Define<string, DateTime>(
LogLevel.Trace,
new EventId(6008, "KlineTrackerKlineUpdated"),
"Kline tracker for {Symbol} kline updated for open time: {LastTime}");
_klineTrackerKlineAdded = LoggerMessage.Define<string, DateTime>(
LogLevel.Trace,
new EventId(6009, "KlineTrackerKlineAdded"),
"Kline tracker for {Symbol} new kline for open time: {LastTime}");
_klineTrackerConnectionLost = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6010, "KlineTrackerConnectionLost"),
"Kline tracker for {Symbol} connection lost");
_klineTrackerConnectionClosed = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6011, "KlineTrackerConnectionClosed"),
"Kline tracker for {Symbol} disconnected");
_klineTrackerConnectionRestored = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6012, "KlineTrackerConnectionRestored"),
"Kline tracker for {Symbol} successfully resynchronized");
_tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug,
new EventId(6013, "KlineTrackerStatusChanged"),
"Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
_tradeTrackerStarting = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6014, "KlineTrackerStarting"),
"Trade tracker for {Symbol} starting");
_tradeTrackerStartFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(6015, "KlineTrackerStartFailed"),
"Trade tracker for {Symbol} failed to start: {Error}");
_tradeTrackerStarted = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6016, "KlineTrackerStarted"),
"Trade tracker for {Symbol} started");
_tradeTrackerStopping = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6017, "KlineTrackerStopping"),
"Trade tracker for {Symbol} stopping");
_tradeTrackerStopped = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6018, "KlineTrackerStopped"),
"Trade tracker for {Symbol} stopped");
_tradeTrackerInitialDataSet = LoggerMessage.Define<string, int, long>(
LogLevel.Debug,
new EventId(6019, "TradeTrackerInitialDataSet"),
"Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}");
_tradeTrackerPreSnapshotSkip = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6020, "TradeTrackerPreSnapshotSkip"),
"Trade tracker for {Symbol} skipping {Id}, already in snapshot");
_tradeTrackerPreSnapshotApplied = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6021, "TradeTrackerPreSnapshotApplied"),
"Trade tracker for {Symbol} adding {Id} from pre-snapshot");
_tradeTrackerTradeAdded = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6022, "TradeTrackerTradeAdded"),
"Trade tracker for {Symbol} adding trade {Id}");
_tradeTrackerConnectionLost = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6023, "TradeTrackerConnectionLost"),
"Trade tracker for {Symbol} connection lost");
_tradeTrackerConnectionClosed = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6024, "TradeTrackerConnectionClosed"),
"Trade tracker for {Symbol} disconnected");
_tradeTrackerConnectionRestored = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6025, "TradeTrackerConnectionRestored"),
"Trade tracker for {Symbol} successfully resynchronized");
}
public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
{
_klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
}
public static void KlineTrackerStarting(this ILogger logger, string symbol)
{
_klineTrackerStarting(logger, symbol, null);
}
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
{
_klineTrackerStartFailed(logger, symbol, error, null);
}
public static void KlineTrackerStarted(this ILogger logger, string symbol)
{
_klineTrackerStarted(logger, symbol, null);
}
public static void KlineTrackerStopping(this ILogger logger, string symbol)
{
_klineTrackerStopping(logger, symbol, null);
}
public static void KlineTrackerStopped(this ILogger logger, string symbol)
{
_klineTrackerStopped(logger, symbol, null);
}
public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerInitialDataSet(logger, symbol, lastTime, null);
}
public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerKlineUpdated(logger, symbol, lastTime, null);
}
public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerKlineAdded(logger, symbol, lastTime, null);
}
public static void KlineTrackerConnectionLost(this ILogger logger, string symbol)
{
_klineTrackerConnectionLost(logger, symbol, null);
}
public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol)
{
_klineTrackerConnectionClosed(logger, symbol, null);
}
public static void KlineTrackerConnectionRestored(this ILogger logger, string symbol)
{
_klineTrackerConnectionRestored(logger, symbol, null);
}
public static void TradeTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
{
_tradeTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
}
public static void TradeTrackerStarting(this ILogger logger, string symbol)
{
_tradeTrackerStarting(logger, symbol, null);
}
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
{
_tradeTrackerStartFailed(logger, symbol, error, null);
}
public static void TradeTrackerStarted(this ILogger logger, string symbol)
{
_tradeTrackerStarted(logger, symbol, null);
}
public static void TradeTrackerStopping(this ILogger logger, string symbol)
{
_tradeTrackerStopping(logger, symbol, null);
}
public static void TradeTrackerStopped(this ILogger logger, string symbol)
{
_tradeTrackerStopped(logger, symbol, null);
}
public static void TradeTrackerInitialDataSet(this ILogger logger, string symbol, int count, long lastId)
{
_tradeTrackerInitialDataSet(logger, symbol, count, lastId, null);
}
public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null);
}
public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null);
}
public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerTradeAdded(logger, symbol, lastId, null);
}
public static void TradeTrackerConnectionLost(this ILogger logger, string symbol)
{
_tradeTrackerConnectionLost(logger, symbol, null);
}
public static void TradeTrackerConnectionClosed(this ILogger logger, string symbol)
{
_tradeTrackerConnectionClosed(logger, symbol, null);
}
public static void TradeTrackerConnectionRestored(this ILogger logger, string symbol)
{
_tradeTrackerConnectionRestored(logger, symbol, null);
}
}
}
+4 -17
View File
@@ -1,6 +1,4 @@
using System.Security;
namespace CryptoExchange.Net.Objects
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Proxy info
@@ -24,14 +22,14 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// The password of the proxy
/// </summary>
public SecureString? Password { get; }
public string? Password { get; }
/// <summary>
/// Create new settings for a proxy
/// </summary>
/// <param name="host">The proxy hostname/ip</param>
/// <param name="port">The proxy port</param>
public ApiProxy(string host, int port): this(host, port, null, (SecureString?)null)
public ApiProxy(string host, int port): this(host, port, null, null)
{
}
@@ -42,18 +40,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, string? password) : this(host, port, login, password?.ToSecureString())
{
}
/// <summary>
/// Create new settings for a proxy
/// </summary>
/// <param name="host">The proxy hostname/ip</param>
/// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, SecureString? password)
public ApiProxy(string host, int port, string? login, string? password)
{
Host = host;
Port = port;
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.Objects
/// Wait for the AutoResetEvent to be set
/// </summary>
/// <returns></returns>
public Task<bool> WaitAsync(TimeSpan? timeout = null)
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
{
lock (_waits)
{
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
}
else
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if(timeout != null)
{
var cancellationSource = new CancellationTokenSource(timeout.Value);
var registration = cancellationSource.Token.Register(() =>
{
lock (_waits)
{
tcs.TrySetResult(false);
if (ct.IsCancellationRequested)
return _completed;
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
if (timeout.HasValue)
{
var timeoutSource = new CancellationTokenSource(timeout.Value);
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
ct = cancellationSource.Token;
}
var registration = ct.Register(() =>
{
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);
return tcs.Task;
}
+132 -4
View File
@@ -1,4 +1,5 @@
using System;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
@@ -273,6 +274,54 @@ namespace CryptoExchange.Net.Objects
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data)
{
return new ExchangeWebResult<K>(exchange, tradeMode, this.As<K>(data));
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data)
{
return new ExchangeWebResult<K>(exchange, tradeModes, this.As<K>(data));
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error returned</param>
/// <returns></returns>
public WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
}
/// <inheritdoc />
public override string ToString()
{
@@ -331,6 +380,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// The data source of this result
/// </summary>
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
/// <summary>
/// Create a new result
/// </summary>
@@ -344,6 +398,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="dataSource"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
@@ -357,6 +412,7 @@ namespace CryptoExchange.Net.Objects
string? requestBody,
HttpMethod? requestMethod,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
ResultDataSource dataSource,
[AllowNull] T data,
Error? error) : base(data, originalData, error)
{
@@ -370,6 +426,7 @@ namespace CryptoExchange.Net.Objects
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
DataSource = dataSource;
}
/// <summary>
@@ -393,7 +450,7 @@ namespace CryptoExchange.Net.Objects
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
@@ -403,7 +460,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
}
/// <summary>
@@ -414,7 +471,78 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <returns></returns>
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode tradeMode)
{
return new ExchangeWebResult<T>(exchange, tradeMode, this);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <returns></returns>
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode[] tradeModes)
{
return new ExchangeWebResult<T>(exchange, tradeModes, this);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageToken">Next page token</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null)
{
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageToken);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageToken">Next page token</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null)
{
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageToken);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult with a specific error
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeError<K>(string exchange, Error error)
{
return new ExchangeWebResult<K>(exchange, null, AsError<K>(error));
}
/// <summary>
/// Return a copy of this result with data source set to cache
/// </summary>
/// <returns></returns>
internal WebCallResult<T> Cached()
{
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
}
/// <inheritdoc />
+4
View File
@@ -13,5 +13,9 @@
/// Form content type header
/// </summary>
public const string FormContentHeader = "application/x-www-form-urlencoded";
/// <summary>
/// Placeholder key for when request body should be set to the value of this KVP
/// </summary>
public const string BodyPlaceHolderKey = "_BODY_";
}
}
+67 -1
View File
@@ -68,6 +68,33 @@
Json
}
/// <summary>
/// Tracker sync status
/// </summary>
public enum SyncStatus
{
/// <summary>
/// Not connected
/// </summary>
Disconnected,
/// <summary>
/// Syncing, data connection is being made
/// </summary>
Syncing,
/// <summary>
/// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment.
/// </summary>
PartiallySynced,
/// <summary>
/// Synced
/// </summary>
Synced,
/// <summary>
/// Disposed
/// </summary>
Diposed
}
/// <summary>
/// Status of the order book
/// </summary>
@@ -152,7 +179,11 @@
/// <summary>
/// Round to closest value
/// </summary>
Closest
Closest,
/// <summary>
/// Round up (ceil)
/// </summary>
Up
}
/// <summary>
@@ -169,4 +200,39 @@
/// </summary>
Snapshot
}
/// <summary>
/// Reconnect policy
/// </summary>
public enum ReconnectPolicy
{
/// <summary>
/// Reconnect is disabled
/// </summary>
Disabled,
/// <summary>
/// Fixed delay of `ReconnectInterval` between retries
/// </summary>
FixedDelay,
/// <summary>
/// Backof policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5
/// </summary>
ExponentialBackoff
}
/// <summary>
/// The data source of the result
/// </summary>
public enum ResultDataSource
{
/// <summary>
/// From server
/// </summary>
Server,
/// <summary>
/// From cache
/// </summary>
Cache
}
}
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option
/// </summary>
public bool CachingEnabled { get; set; } = false;
/// <summary>
/// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done
/// </summary>
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Create a copy of this options
/// </summary>
@@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
Proxy = Proxy,
RequestTimeout = RequestTimeout,
RateLimiterEnabled = RateLimiterEnabled,
RateLimitingBehaviour = RateLimitingBehaviour
RateLimitingBehaviour = RateLimitingBehaviour,
CachingEnabled = CachingEnabled,
CachingMaxAge = CachingMaxAge,
};
}
}
@@ -9,15 +9,15 @@ namespace CryptoExchange.Net.Objects.Options
public class SocketExchangeOptions : ExchangeOptions
{
/// <summary>
/// Whether or not the socket should automatically reconnect when losing connection
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// Time to wait between reconnect attempts
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Reconnect policy
/// </summary>
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
/// <summary>
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
/// </summary>
@@ -46,6 +46,12 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
/// <summary>
/// This delay is used to set a RetryAfter guard on the connection after a rate limit is hit on the server.
/// This is used to prevent the client from reconnecting too quickly after a rate limit is hit.
/// </summary>
public TimeSpan? ConnectDelayAfterRateLimited { get; set; }
/// <summary>
/// Create a copy of this options
/// </summary>
@@ -57,7 +63,7 @@ namespace CryptoExchange.Net.Objects.Options
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoReconnect = AutoReconnect,
ReconnectPolicy = ReconnectPolicy,
DelayAfterConnect = DelayAfterConnect,
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
ReconnectInterval = ReconnectInterval,
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects
{
@@ -148,6 +149,27 @@ namespace CryptoExchange.Net.Objects
Add(key, DateTimeConverter.ConvertToSeconds(value));
}
/// <summary>
/// Add a datetime value as string seconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddSecondsString(string key, DateTime value)
{
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
}
/// <summary>
/// Add a datetime value as string seconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalSecondsString(string key, DateTime? value)
{
if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
@@ -166,7 +188,7 @@ namespace CryptoExchange.Net.Objects
public void AddEnumAsInt<T>(string key, T value)
{
var stringVal = EnumConverter.GetString(value);
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
Add(key, int.Parse(stringVal)!);
}
/// <summary>
@@ -193,5 +215,18 @@ namespace CryptoExchange.Net.Objects
Add(key, int.Parse(stringVal));
}
}
/// <summary>
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
/// </summary>
/// <param name="body">Body to set</param>
/// <exception cref="InvalidOperationException"></exception>
public void SetBody(object body)
{
if (this.Any())
throw new InvalidOperationException("Can't set body when other parameters already specified");
Add(Constants.BodyPlaceHolderKey, body);
}
}
}
@@ -1,5 +1,4 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
@@ -48,18 +47,26 @@ namespace CryptoExchange.Net.Objects
/// Request weight
/// </summary>
public int Weight { get; set; } = 1;
/// <summary>
/// Rate limit gate to use
/// </summary>
public IRateLimitGate? RateLimitGate { get; set; }
/// <summary>
/// Rate limit for this specific endpoint
/// Individual endpoint rate limit guard to use
/// </summary>
public int? EndpointLimitCount { get; set; }
public IRateLimitGuard? LimitGuard { get; set; }
/// <summary>
/// Rate limit period for this specific endpoint
/// Whether this request should never be cached
/// </summary>
public TimeSpan? EndpointLimitPeriod { get; set; }
public bool PreventCaching { get; set; }
/// <summary>
/// Connection id
/// </summary>
public int? ConnectionId { get; set; }
/// <summary>
/// ctor
@@ -1,7 +1,5 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
@@ -41,13 +39,13 @@ namespace CryptoExchange.Net.Objects
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="endpointLimitCount">The limit count for this specific endpoint</param>
/// <param name="endpointLimitPeriod">The period for the limit for this specific endpoint</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <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(
HttpMethod method,
@@ -55,11 +53,11 @@ namespace CryptoExchange.Net.Objects
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
int? endpointLimitCount = null,
TimeSpan? endpointLimitPeriod = null,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null)
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{
if (!_definitions.TryGetValue(method + path, out var def))
@@ -67,13 +65,13 @@ namespace CryptoExchange.Net.Objects
def = new RequestDefinition(path, method)
{
Authenticated = authenticated,
EndpointLimitCount = endpointLimitCount,
EndpointLimitPeriod = endpointLimitPeriod,
LimitGuard = limitGuard,
RateLimitGate = rateLimitGate,
Weight = weight,
ArraySerialization = arraySerialization,
RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false
};
_definitions.TryAdd(method + path, def);
}
+102 -12
View File
@@ -1,4 +1,5 @@
using System;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Objects.Sockets
{
@@ -14,9 +15,14 @@ namespace CryptoExchange.Net.Objects.Sockets
public DateTime Timestamp { get; set; }
/// <summary>
/// The topic of the update, what symbol/asset etc..
/// The stream producing the update
/// </summary>
public string? Topic { get; set; }
public string? StreamId { get; set; }
/// <summary>
/// The symbol the update is for
/// </summary>
public string? Symbol { get; set; }
/// <summary>
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
@@ -33,10 +39,14 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public T Data { get; set; }
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
/// <summary>
/// ctor
/// </summary>
public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
{
Data = data;
Topic = topic;
StreamId = streamId;
Symbol = symbol;
OriginalData = originalData;
Timestamp = timestamp;
UpdateType = updateType;
@@ -50,7 +60,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public DataEvent<K> As<K>(K data)
{
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType);
return new DataEvent<K>(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType);
}
/// <summary>
@@ -58,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <param name="topic">The new topic</param>
/// <param name="symbol">The new symbol</param>
/// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic)
public DataEvent<K> As<K>(K data, string? symbol)
{
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType);
return new DataEvent<K>(data, StreamId, symbol, OriginalData, Timestamp, UpdateType);
}
/// <summary>
@@ -70,12 +80,92 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <param name="topic">The new topic</param>
/// <param name="streamId">The new stream id</param>
/// <param name="symbol">The new symbol</param>
/// <param name="updateType">The type of update</param>
/// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType)
public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
{
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType);
return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange the result is for</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data)
{
return new ExchangeEvent<K>(exchange, this.As<K>(data));
}
/// <summary>
/// Specify the symbol
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
public DataEvent<T> WithSymbol(string symbol)
{
Symbol = symbol;
return this;
}
/// <summary>
/// Specify the update type
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public DataEvent<T> WithUpdateType(SocketUpdateType type)
{
UpdateType = type;
return this;
}
/// <summary>
/// Specify the stream id
/// </summary>
/// <param name="streamId"></param>
/// <returns></returns>
public DataEvent<T> WithStreamId(string streamId)
{
StreamId = streamId;
return this;
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<T> ToCallResult()
{
return new CallResult<T>(Data, OriginalData, null);
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<K> ToCallResult<K>(K data)
{
return new CallResult<K>(data, OriginalData, null);
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<K> ToCallResult<K>(Error error)
{
return new CallResult<K>(default, OriginalData, error);
}
/// <inheritdoc />
public override string ToString()
{
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}";
}
}
}
@@ -30,6 +30,15 @@ namespace CryptoExchange.Net.Objects.Sockets
remove => _connection.ConnectionClosed -= value;
}
/// <summary>
/// Event when a lost connection is restored, but the resubscribing of update subscriptions failed
/// </summary>
public event Action<Error> ResubscribingFailed
{
add => _connection.ResubscribingFailed += value;
remove => _connection.ResubscribingFailed -= value;
}
/// <summary>
/// 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
@@ -26,20 +26,20 @@ namespace CryptoExchange.Net.Objects.Sockets
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
/// <summary>
/// The time to wait between reconnect attempts
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Reconnect policy
/// </summary>
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
/// <summary>
/// Proxy for the connection
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Whether the socket should automatically reconnect when connection is lost
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
/// </summary>
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// ctor
/// </summary>
/// <param name="uri">Uri</param>
/// <param name="autoReconnect">Auto reconnect</param>
public WebSocketParameters(Uri uri, bool autoReconnect)
/// <param name="policy">Reconnect policy</param>
public WebSocketParameters(Uri uri, ReconnectPolicy policy)
{
Uri = uri;
AutoReconnect = autoReconnect;
ReconnectPolicy = policy;
}
}
}
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.OrderBook
@@ -8,14 +9,14 @@ namespace CryptoExchange.Net.OrderBook
public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions
{
private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor;
private readonly Func<string, string, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
private readonly Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
/// <summary>
/// ctor
/// </summary>
/// <param name="symbolCtor"></param>
/// <param name="assetsCtor"></param>
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<string, string, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
{
_symbolCtor = symbolCtor;
_assetsCtor = assetsCtor;
@@ -25,6 +26,9 @@ namespace CryptoExchange.Net.OrderBook
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options);
/// <inheritdoc />
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(baseAsset, quoteAsset, options);
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset), options);
/// <inheritdoc />
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null) => _assetsCtor(symbol, options);
}
}
@@ -810,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
{
if (lastUpdateId <= LastSequenceNumber)
{
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
return;
}
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
@@ -21,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> definition.Authenticated == _authenticated;
}
}
@@ -1,9 +1,6 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Filters
{
@@ -24,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -1,7 +1,6 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Collections.Generic;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
@@ -22,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> _paths.Contains(definition.Path);
}
}
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
@@ -21,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> host == _host;
}
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
@@ -21,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> type == _type;
}
}
@@ -1,7 +1,6 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters
{
@@ -22,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -3,8 +3,6 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Guards
{
@@ -14,26 +12,30 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary>
/// Apply guard per host
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host);
/// <summary>
/// Apply guard per endpoint
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
/// <summary>
/// Apply guard per connection
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString());
/// <summary>
/// Apply guard per API key
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
/// <summary>
/// Apply guard per API key per endpoint
/// </summary>
public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method);
private readonly IEnumerable<IGuardFilter> _filters;
private readonly Dictionary<string, IWindowTracker> _trackers;
private RateLimitWindowType _windowType;
private double? _decayRate;
private int? _connectionWeight;
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
/// <inheritdoc />
public string Name => "RateLimitGuard";
@@ -60,7 +62,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
{
}
@@ -75,7 +77,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
{
_filters = filters;
_trackers = new Dictionary<string, IWindowTracker>();
@@ -88,7 +90,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{
foreach(var filter in _filters)
{
@@ -114,7 +116,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{
foreach (var filter in _filters)
{
@@ -1,9 +1,6 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Guards
{
@@ -21,25 +18,35 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "RetryAfterGuard";
/// <inheritdoc />
public string Description => $"Pause requests until after {After}";
public string Description => $"Pause {Type} until after {After}";
/// <summary>
/// The timestamp after which requests are allowed again
/// </summary>
public DateTime After { get; private set; }
/// <summary>
/// The type of rate limit item this guard is for
/// </summary>
public RateLimitItemType Type { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="after"></param>
public RetryAfterGuard(DateTime after)
/// <param name="type"></param>
public RetryAfterGuard(DateTime after, RateLimitItemType type)
{
After = after;
Type = type;
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{
if (type != Type)
return LimitCheck.NotApplicable;
var dif = (After + _windowBuffer) - DateTime.UtcNow;
if (dif <= TimeSpan.Zero)
return LimitCheck.NotApplicable;
@@ -48,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{
return RateLimitState.NotApplied;
}
@@ -3,7 +3,6 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using System;
using System.Collections.Generic;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Guards
{
@@ -12,9 +11,22 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// </summary>
public class SingleLimitGuard : IRateLimitGuard
{
/// <summary>
/// Default endpoint limit
/// </summary>
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
/// <summary>
/// Endpoint limit per API key
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate;
private readonly int _limit;
private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
/// <inheritdoc />
public string Name => "EndpointLimitGuard";
@@ -25,20 +37,28 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary>
/// ctor
/// </summary>
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
public SingleLimitGuard(
int limit,
TimeSpan period,
RateLimitWindowType windowType,
double? decayRate = null,
Func<RequestDefinition, string, string?, string>? keySelector = null)
{
_limit = limit;
_period = period;
_windowType = windowType;
_decayRate = decayRate;
_keySelector = keySelector ?? Default;
_trackers = new Dictionary<string, IWindowTracker>();
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{
var key = definition.Path + definition.Method;
var key = _keySelector(definition, host, apiKey);
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
tracker = CreateTracker();
_trackers.Add(key, tracker);
}
@@ -46,27 +66,27 @@ namespace CryptoExchange.Net.RateLimiting.Guards
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
{
var key = definition.Path + definition.Method;
var key = _keySelector(definition, host, apiKey);
var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
return RateLimitState.Applied(_limit, _period, tracker.Current);
}
/// <summary>
/// Create a new WindowTracker
/// </summary>
/// <returns></returns>
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
protected IWindowTracker CreateTracker()
{
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
}
}
}
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Objects;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Interfaces
{
@@ -16,6 +15,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <returns>True if passed</returns>
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey);
}
}
@@ -1,8 +1,6 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Guards;
using Microsoft.Extensions.Logging;
using System;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
@@ -29,15 +27,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
/// </summary>
/// <param name="retryAfter">The time after which requests can be send again</param>
/// <param name="type">RateLimitType</param>
/// <returns></returns>
Task SetRetryAfterGuardAsync(DateTime retryAfter);
/// <summary>
/// Set the SingleLimitGuard for handling individual endpoint rate limits
/// </summary>
/// <param name="guard"></param>
/// <returns></returns>
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request);
/// <summary>
/// Returns the 'retry after' timestamp if set
@@ -58,21 +50,21 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
/// <summary>
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="itemId">Id of the item to check</param>
/// <param name="guard">The guard</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct);
}
}
@@ -1,6 +1,4 @@
using CryptoExchange.Net.Objects;
using System.Net.Http;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Interfaces
{
@@ -28,7 +26,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight);
/// <summary>
/// Apply the request to this guard with the specified weight
@@ -39,6 +37,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight);
}
}
@@ -7,7 +7,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
@@ -16,7 +15,6 @@ namespace CryptoExchange.Net.RateLimiting
/// <inheritdoc />
public class RateLimitGate : IRateLimitGate
{
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
private readonly ConcurrentBag<IRateLimitGuard> _guards;
private readonly SemaphoreSlim _semaphore;
private readonly string _name;
@@ -33,45 +31,67 @@ namespace CryptoExchange.Net.RateLimiting
{
_name = name;
_guards = new ConcurrentBag<IRateLimitGuard>();
_semaphore = new SemaphoreSlim(1);
_semaphore = new SemaphoreSlim(1, 1);
}
/// <inheritdoc />
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++;
try
{
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally
{
_waitingCount--;
_semaphore.Release();
if (release)
_semaphore.Release();
}
}
/// <inheritdoc />
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
public async Task<CallResult> ProcessSingleAsync(
ILogger logger,
int itemId,
IRateLimitGuard guard,
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
RateLimitingBehaviour rateLimitingBehaviour,
CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
if (requestWeight == 0)
requestWeight = 1;
bool release = true;
_waitingCount++;
try
{
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally
{
_waitingCount--;
_semaphore.Release();
if (release)
_semaphore.Release();
}
}
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{
foreach (var guard in guards)
{
@@ -101,7 +121,7 @@ namespace CryptoExchange.Net.RateLimiting
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
@@ -131,14 +151,7 @@ namespace CryptoExchange.Net.RateLimiting
}
/// <inheritdoc />
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
{
_singleLimitGuard = guard;
return this;
}
/// <inheritdoc />
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type)
{
await _semaphore.WaitAsync().ConfigureAwait(false);
@@ -146,7 +159,7 @@ namespace CryptoExchange.Net.RateLimiting
{
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
if (retryAfterGuard == null)
_guards.Add(new RetryAfterGuard(retryAfter));
_guards.Add(new RetryAfterGuard(retryAfter, type));
else
retryAfterGuard.UpdateAfter(retryAfter);
}
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private TimeSpan DetermineWaitTime(int requestWeight)
{
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
}
}
}
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private TimeSpan DetermineWaitTime()
{
var checkTime = DateTime.UtcNow;
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
}
}
}
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
return wait.Add(_fixedWindowBuffer);
var result = wait.Add(_fixedWindowBuffer);
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
}
}
}
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private readonly List<LimitEntry> _entries;
private int _currentWeight = 0;
/// <summary>
/// Additional wait time to apply to account for fluctuating request times
/// </summary>
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
public SlidingWindowTracker(int limit, TimeSpan period)
{
Limit = limit;
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
removedWeight += entry.Weight;
if (removedWeight >= weightToRemove)
{
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
if (result < TimeSpan.Zero)
return TimeSpan.Zero;
return result;
}
}
@@ -19,6 +19,12 @@ namespace CryptoExchange.Net.Requests
if (client == null)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
@@ -0,0 +1,29 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Fee asset selection type
/// </summary>
public enum SharedFeeAssetType
{
/// <summary>
/// Fee is always in the base asset
/// </summary>
BaseAsset,
/// <summary>
/// Fee is always in the quote asset
/// </summary>
QuoteAsset,
/// <summary>
/// Fee is always in the input asset
/// </summary>
InputAsset,
/// <summary>
/// Fee is always in the output asset
/// </summary>
OutputAsset,
/// <summary>
/// Fee is variable
/// </summary>
Variable
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Fee deduction type
/// </summary>
public enum SharedFeeDeductionType
{
/// <summary>
/// The fee is deducted from the output amount. For example buying 1 ETH at 1000 USDT with a 1% fee would cost 1000 USDT and output 0.99 ETH
/// </summary>
DeductFromOutput,
/// <summary>
/// The fee is added to the order cost. For example buying 1 ETH at 1000 USDT with a 1% fee would cost 1010 USDT and output 1 ETH
/// </summary>
AddToCost
}
}
@@ -0,0 +1,33 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Kline interval
/// </summary>
public enum SharedKlineInterval
{
/// <summary>
/// 5 min
/// </summary>
FiveMinutes = 60 * 5,
/// <summary>
/// 15 min
/// </summary>
FifteenMinutes = 60 * 15,
/// <summary>
/// 1 hour
/// </summary>
OneHour = 60 * 60,
/// <summary>
/// 1 day
/// </summary>
OneDay = 60 * 60 * 24,
/// <summary>
/// 1 week
/// </summary>
OneWeek = 60 * 60 * 24 * 7,
/// <summary>
/// 1 month
/// </summary>
OneMonth = 60 * 60 * 24 * 30
}
}
@@ -0,0 +1,21 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Leverage setting mode
/// </summary>
public enum SharedLeverageSettingMode
{
/// <summary>
/// Leverage is configured per side (in hedge mode)
/// </summary>
PerSide,
/// <summary>
/// Leverage is configured for the symbol
/// </summary>
PerSymbol,
/// <summary>
/// Leverage is configured for the entire account
/// </summary>
PerAccount
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Margin mode
/// </summary>
public enum SharedMarginMode
{
/// <summary>
/// Cross margin, margin is shared across symbols
/// </summary>
Cross,
/// <summary>
/// Isolated margin, margin is isolated on a symbol
/// </summary>
Isolated
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Side of an order
/// </summary>
public enum SharedOrderSide
{
/// <summary>
/// Buy order
/// </summary>
Buy,
/// <summary>
/// Sell order
/// </summary>
Sell
}
}
@@ -0,0 +1,21 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Status of an order
/// </summary>
public enum SharedOrderStatus
{
/// <summary>
/// Order is open waiting to be filled
/// </summary>
Open,
/// <summary>
/// Order has been fully filled
/// </summary>
Filled,
/// <summary>
/// Order has been canceled
/// </summary>
Canceled
}
}
@@ -0,0 +1,25 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Type of an order
/// </summary>
public enum SharedOrderType
{
/// <summary>
/// Limit order, execute at a specific price
/// </summary>
Limit,
/// <summary>
/// Limit maker order, a limit order with the condition that is will never be executed as a maker
/// </summary>
LimitMaker,
/// <summary>
/// Market order, execute at the best price currently available
/// </summary>
Market,
/// <summary>
/// Other order type, used for parsing unsupported order types
/// </summary>
Other
}
}
@@ -0,0 +1,21 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Supported pagination type
/// </summary>
public enum SharedPaginationSupport
{
/// <summary>
/// Pagination is not supported for this exchange request
/// </summary>
NotSupported,
/// <summary>
/// Pagination is in ascending order
/// </summary>
Ascending,
/// <summary>
/// Pagination is in descending order
/// </summary>
Descending
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Position mode
/// </summary>
public enum SharedPositionMode
{
/// <summary>
/// Hedge mode, a symbol can have both a long and a short position at the same time
/// </summary>
HedgeMode,
/// <summary>
/// One way mode, a symbol can only have one open position side at a time
/// </summary>
OneWay
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Position mode selection type
/// </summary>
public enum SharedPositionModeSelection
{
/// <summary>
/// Position mode is configured per symbol
/// </summary>
PerSymbol,
/// <summary>
/// Position mode is configured for the entire account
/// </summary>
PerAccount
}
}
@@ -0,0 +1,17 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// The side of a position
/// </summary>
public enum SharedPositionSide
{
/// <summary>
/// Long position
/// </summary>
Long,
/// <summary>
/// Short position
/// </summary>
Short
}
}
@@ -0,0 +1,25 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Quote asset order quantity support
/// </summary>
public enum SharedQuantityType
{
/// <summary>
/// Quantity should be in the base asset
/// </summary>
BaseAsset,
/// <summary>
/// Quantity should be in the quote asset
/// </summary>
QuoteAsset,
/// <summary>
/// Quantity is in the number of contracts
/// </summary>
Contracts,
/// <summary>
/// Quantity can be either base or quote quantity
/// </summary>
BaseAndQuoteAsset
}
}

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