mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
202 Commits
7.5.2
...
9.0.0-beta4
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d6267da93 | |||
| 8def7f32af | |||
| ac295de9f6 | |||
| d412e0895e | |||
| 1f9e2b4fcb | |||
| b13cff5a95 | |||
| 4c050744ad | |||
| 3b15c35a02 | |||
| cd78dbf575 | |||
| a258532d6a | |||
| d2a87a1069 | |||
| e07f24ea0a | |||
| 024e8dcfe2 | |||
| 4bb5aae40a | |||
| dec94678ec | |||
| 1a49fc8251 | |||
| 29b0875960 | |||
| 976ccab1da | |||
| 02bbd37bb6 | |||
| 1bbbec7f2b | |||
| 0262f04913 | |||
| fd1ec17d72 | |||
| 4bdad7fe0c | |||
| 74f73dc790 | |||
| 0527a8a76e | |||
| c693eb8c02 | |||
| 3eb28c7fed | |||
| 618c4922b9 | |||
| c81b15861d | |||
| 4a5832cccd | |||
| 4e47c4cbdf | |||
| 2af1520ecc | |||
| cf397af3ab | |||
| a1479705e2 | |||
| 175e23f110 | |||
| 9b7019ded2 | |||
| 7904aa9ba7 | |||
| 3fe6db589f | |||
| 625dccbbe4 | |||
| e650771d16 | |||
| 3dad28b19d | |||
| 2b9fda985e | |||
| ff8759409b | |||
| 0d9627c13f | |||
| 0179fd7e2a | |||
| b8d0b0cf95 | |||
| 73c42bd452 | |||
| 290be7f5e0 | |||
| 0be1bb16e3 | |||
| 8605196390 | |||
| 460dd97537 | |||
| 1ec5984fad | |||
| 8260c2661d | |||
| 591c1dd405 | |||
| 0164cdfcc4 | |||
| 23a6cfff87 | |||
| fdcdb90a5f | |||
| 0b7107401f | |||
| 06add65354 | |||
| 773d288497 | |||
| fd4e8da938 | |||
| 271743b669 | |||
| f4797caf37 | |||
| 62c9769c72 | |||
| 92d7bc1e2e | |||
| 99e4f96f63 | |||
| 94d8afe149 | |||
| 90ad59c63a | |||
| c2273edfaa | |||
| 236283f4dd | |||
| b66f12ff75 | |||
| 0403384beb | |||
| 7d7bc35869 | |||
| 48797038be | |||
| d21792d04c | |||
| 8414e9d94f | |||
| ab0243445d | |||
| f2cf70b02f | |||
| 9ff417bba8 | |||
| 6b43d08a4d | |||
| 39bf7fe9b9 | |||
| b5893c3b60 | |||
| 15657ba683 | |||
| 1aed9f0c67 | |||
| 17f1560310 | |||
| 41de0a3150 | |||
| 3e410be611 | |||
| be75449e4a | |||
| b1b05c8f6b | |||
| a0e588c3de | |||
| 9e86a08327 | |||
| ed007b5272 | |||
| bdd5526244 | |||
| ce35e30688 | |||
| b40f72b1b0 | |||
| 31a6cf285b | |||
| 1842f4fda0 | |||
| 7a58902ab6 | |||
| 3cb91296ca | |||
| 130ed40580 | |||
| 94cb2caf0b | |||
| 917d060827 | |||
| c58bc2be07 | |||
| ff3356e2b4 | |||
| 79434c7be5 | |||
| 168dabc11f | |||
| 71ee263683 | |||
| 7239b9c289 | |||
| 84d36544e4 | |||
| a71f57ae7f | |||
| 6e5bcd5e9a | |||
| 4131e563c3 | |||
| 613766dbca | |||
| 23b07d709e | |||
| bbbdac2fd3 | |||
| c614b7869c | |||
| 1f31e4a9d7 | |||
| 6cb6cd6b11 | |||
| 17ffec329f | |||
| 7a3927ef49 | |||
| c1b0437c93 | |||
| 23e947f258 | |||
| b8686d60b9 | |||
| 5d3de52da6 | |||
| fee18fd183 | |||
| 3a43d461a3 | |||
| 5f409efad3 | |||
| cc1f0796fe | |||
| b1cd9b5412 | |||
| 42003a0247 | |||
| d89c2bde94 | |||
| 3e6bdaafc6 | |||
| 93e4722a81 | |||
| 355ecb03da | |||
| 994c527c1d | |||
| 69b2e2045e | |||
| 7fde8bf5da | |||
| 637070a7ae | |||
| 7be75f72a7 | |||
| e3fece41f3 | |||
| 87b0c8d7a2 | |||
| ca9a711f22 | |||
| 27597bc994 | |||
| 776d75170d | |||
| 949780a9ad | |||
| 2f64cd9f05 | |||
| 185dfeb6fb | |||
| 68067d6258 | |||
| b309deb0c4 | |||
| e1dafdf0dd | |||
| fd7b5f0f0f | |||
| 3b735d66fd | |||
| 3cd505ac8b | |||
| 81d856d78d | |||
| 11c1ad871a | |||
| ffcb7db8ff | |||
| 02432e5109 | |||
| a85bfb4432 | |||
| 17d85fdd85 | |||
| 5e0733d7f4 | |||
| 28d5287bd4 | |||
| ef5097589a | |||
| f287ec1fa4 | |||
| 28da93af9d | |||
| 0d5bdf5095 | |||
| 8dac3d7aa6 | |||
| 6951f31be7 | |||
| 630f85ec49 | |||
| 9ec4f2276f | |||
| 0a0c66541e | |||
| bb4199620e | |||
| 8a83cd2cb8 | |||
| fcfeaf568f | |||
| 25567ea434 | |||
| 1ab85d4c26 | |||
| be68115099 | |||
| ff0550b0fb | |||
| 1ab1e008fc | |||
| 6f30c72608 | |||
| e927bc3d20 | |||
| 09ed7d1436 | |||
| 6fed657ea6 | |||
| 1555f8da0c | |||
| 68b28fc875 | |||
| 5d50d8cde8 | |||
| 9ff673d8be | |||
| 3e5a34fb56 | |||
| 64ee50d98c | |||
| 6a105c6f8f | |||
| 287aadc720 | |||
| 7229438a0b | |||
| 444af98a15 | |||
| 70c6fa1bbb | |||
| d27f394b46 | |||
| c8c98e13d0 | |||
| 9fcd722991 | |||
| 8080ecccc0 | |||
| 4b6fa9a1b1 | |||
| 0b6dbde7d4 | |||
| fe4d63ba75 | |||
| 04bd3727ca | |||
| 7e6fcd03c2 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -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
|
||||
@@ -106,6 +106,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
for(var i = 1; i <= 10; i++)
|
||||
{
|
||||
evnt.Set();
|
||||
await Task.Delay(1); // Wait for the continuation.
|
||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
}
|
||||
|
||||
|
||||
@@ -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>net9.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.12.0"></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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = ExchangeHelpers.Normalize(input);
|
||||
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("123", "BKR", 32, true, "BKRJK123")]
|
||||
[TestCase("123", "BKR", 32, false, "123")]
|
||||
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
|
||||
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
|
||||
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
|
||||
[TestCase(null, "BKR", 32, true, null)]
|
||||
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
|
||||
{
|
||||
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
|
||||
|
||||
if (expected != null)
|
||||
Assert.That(result, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, null, 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, null, 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, null, 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, null, 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, null, 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, null, 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, null, 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, null, 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, null, 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, null, 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, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -328,9 +328,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, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -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, null, 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, null, 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, null, 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, null, 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, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, 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;
|
||||
|
||||
@@ -19,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
var options = new TestClientOptions();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
@@ -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,8 @@ using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -23,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
}
|
||||
Initialize(options.Value);
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@@ -138,7 +135,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 +179,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,40 +13,37 @@ 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;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
internal class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
Initialize(options.Value);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.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/");
|
||||
}
|
||||
|
||||
@@ -69,12 +66,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestSocketOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
|
||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
||||
{
|
||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
|
||||
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 +98,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 +123,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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
|
||||
/// <summary>
|
||||
/// Map a enum entry to string values
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if !NETSTANDARD2_1
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
|
||||
@@ -9,71 +8,37 @@ 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; set; }
|
||||
|
||||
/// <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; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
public ApiCredentialsType CredentialType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <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)
|
||||
/// <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="credentialType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||
{
|
||||
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();
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,8 +47,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
return new ApiCredentials(Key, Secret, CredentialType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="inputStream">The stream containing the json data</param>
|
||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
||||
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
{
|
||||
var accessor = new SystemTextJsonStreamMessageAccessor();
|
||||
if (!accessor.Read(inputStream, false).Result)
|
||||
@@ -102,20 +66,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
|
||||
|
||||
inputStream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Secret?.Dispose();
|
||||
return new ApiCredentials(key, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 +28,22 @@ 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>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
if (credentials.Secret == null)
|
||||
if (credentials.Key == null || credentials.Secret == null)
|
||||
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,
|
||||
@@ -364,9 +369,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_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
|
||||
{
|
||||
@@ -398,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
return Convert.ToHexString(buff);
|
||||
#else
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -434,10 +443,28 @@ namespace CryptoExchange.Net.Authentication
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the serialized request body
|
||||
/// </summary>
|
||||
/// <param name="serializer"></param>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||
{
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
return serializer.Serialize(value);
|
||||
else
|
||||
return serializer.Serialize(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,9 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
@@ -50,7 +55,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
@@ -65,10 +70,7 @@ namespace CryptoExchange.Net.Clients
|
||||
BaseAddress = baseAddress;
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,16 +81,25 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||
{
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
||||
if (options.ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -97,7 +108,6 @@ namespace CryptoExchange.Net.Clients
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 readonly 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>
|
||||
@@ -84,6 +109,16 @@ namespace CryptoExchange.Net.Clients
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
@@ -120,6 +122,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
result.Add(client.GetState());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public class CryptoBaseClient : IDisposable
|
||||
{
|
||||
private Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
||||
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Service provider
|
||||
|
||||
@@ -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 readonly static MemoryCache _cache = new MemoryCache();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -148,35 +154,114 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <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>
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? parameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null) where T : class
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null,
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||
return SendAsync<T>(
|
||||
baseAddress,
|
||||
definition,
|
||||
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
||||
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||
cancellationToken,
|
||||
additionalHeaders,
|
||||
weight,
|
||||
weightSingleLimiter,
|
||||
rateLimitKeySuffix);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
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,
|
||||
int? weightSingleLimiter = null,
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
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, weightSingleLimiter, rateLimitKeySuffix).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)
|
||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
||||
if (result.Error is not CancellationRequestedError)
|
||||
{
|
||||
if (!result)
|
||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString());
|
||||
else
|
||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
||||
}
|
||||
else
|
||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]");
|
||||
{
|
||||
_logger.RestApiCancellationRequested(result.RequestId);
|
||||
}
|
||||
|
||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
if (result.Success &&
|
||||
ShouldCache(definition))
|
||||
{
|
||||
_cache.Add(cacheKey!, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -184,25 +269,26 @@ 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>
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector</param>
|
||||
/// <returns></returns>
|
||||
/// <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)
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null,
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
var requestWeight = weight ?? definition.Weight;
|
||||
|
||||
// Time sync
|
||||
if (definition.Authenticated)
|
||||
{
|
||||
@@ -228,6 +314,7 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
var requestWeight = weight ?? definition.Weight;
|
||||
if (requestWeight != 0)
|
||||
{
|
||||
if (definition.RateLimitGate == null)
|
||||
@@ -235,21 +322,22 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult(limitResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 singleRequestWeight = weightSingleLimiter ?? 1;
|
||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult(limitResult.Error!);
|
||||
}
|
||||
@@ -261,27 +349,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 +381,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 +396,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 +428,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 +502,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 +516,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 +552,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;
|
||||
}
|
||||
}
|
||||
@@ -513,7 +623,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, null, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult<IRequest>(limitResult.Error!);
|
||||
}
|
||||
@@ -528,7 +638,7 @@ namespace CryptoExchange.Net.Clients
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
|
||||
var headers = request.GetHeaders();
|
||||
if (headers.Any())
|
||||
if (headers.Count != 0)
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
|
||||
TotalRequestsMade++;
|
||||
@@ -588,47 +698,58 @@ 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);
|
||||
var parsedError = TryParseError(response.ResponseHeaders, accessor);
|
||||
if (parsedError != null)
|
||||
{
|
||||
if (parsedError is ServerRateLimitError rateError)
|
||||
{
|
||||
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
|
||||
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Success status code, but TryParseError determined it was an error response
|
||||
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
|
||||
@@ -641,12 +762,13 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
|
||||
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
|
||||
/// This method will be called for each response to be able to check if the response is an error or not.
|
||||
/// If the response is an error this method should return the parsed error, else it should return null
|
||||
/// </summary>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <returns>Null if not an error, Error otherwise</returns>
|
||||
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null;
|
||||
protected virtual Error? TryParseError(IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor) => null;
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||
@@ -663,7 +785,7 @@ namespace CryptoExchange.Net.Clients
|
||||
// Only retry once
|
||||
return false;
|
||||
|
||||
if ((int?)callResult.ResponseStatusCode == 429
|
||||
if (callResult.Error is ServerRateLimitError
|
||||
&& ClientOptions.RateLimiterEnabled
|
||||
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
|
||||
&& gate != null)
|
||||
@@ -718,12 +840,12 @@ namespace CryptoExchange.Net.Clients
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||
{
|
||||
foreach (var parameter in parameters)
|
||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||
uri = uri.AddQueryParameter(parameter.Key, parameter.Value.ToString()!);
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
var 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 +854,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 +869,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 +901,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 +921,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.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
stringData = CreateSerializer().Serialize(value);
|
||||
else
|
||||
stringData = CreateSerializer().Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
@@ -873,18 +994,26 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns>Server time</returns>
|
||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetOptions<T>(UpdateOptions<T> options)
|
||||
{
|
||||
base.SetOptions(options);
|
||||
|
||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
|
||||
}
|
||||
|
||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||
{
|
||||
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 +1042,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 +67,22 @@ 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
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
if (socketConnections.IsEmpty)
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||
@@ -92,7 +97,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
if (socketConnections.IsEmpty)
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
||||
@@ -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>
|
||||
@@ -143,7 +158,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="queryDelegate"></param>
|
||||
/// <param name="callback"></param>
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
|
||||
{
|
||||
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
||||
{
|
||||
@@ -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);
|
||||
@@ -384,9 +422,10 @@ namespace CryptoExchange.Net.Clients
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult(result.Error)!;
|
||||
}
|
||||
|
||||
_logger.Authenticated(socket.SocketId);
|
||||
}
|
||||
|
||||
_logger.Authenticated(socket.SocketId);
|
||||
socket.Authenticated = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
@@ -395,7 +434,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 +483,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 +529,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 +556,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,14 +593,15 @@ 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,
|
||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||
RateLimitingBehaviour = ClientOptions.RateLimitingBehaviour,
|
||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -592,8 +671,11 @@ 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)
|
||||
{
|
||||
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
|
||||
tasks.Add(connection.CloseAsync(subscription));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
@@ -616,6 +698,42 @@ 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetOptions<T>(UpdateOptions<T> options)
|
||||
{
|
||||
var previousProxyIsSet = ClientOptions.Proxy != null;
|
||||
base.SetOptions(options);
|
||||
|
||||
if ((!previousProxyIsSet && options.Proxy == null)
|
||||
|| socketConnections.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
||||
|
||||
// Update proxy, also triggers reconnect
|
||||
foreach (var connection in socketConnections)
|
||||
_ = connection.Value.UpdateProxy(options.Proxy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
@@ -699,11 +817,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 +843,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
|
||||
var result = Activator.CreateInstance(objectType);
|
||||
var arr = JArray.Load(reader);
|
||||
return ParseObject(arr, result, objectType);
|
||||
return ParseObject(arr, result!, objectType);
|
||||
}
|
||||
|
||||
private static object ParseObject(JArray arr, object result, Type objectType)
|
||||
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
var count = 0;
|
||||
if (innerArray.Count == 0)
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else if (innerArray[0].Type == JTokenType.Array)
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
|
||||
foreach (var obj in innerArray)
|
||||
{
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
|
||||
count++;
|
||||
}
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
continue;
|
||||
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
object? value;
|
||||
if (converterAttribute != null)
|
||||
{
|
||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
|
||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
|
||||
}
|
||||
else if (conversionAttribute != null)
|
||||
{
|
||||
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
}
|
||||
else if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
&& (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
var v = value.ToString();
|
||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
last = arrayProp.Index;
|
||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
||||
if (converterAttribute != null)
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
|
||||
else if (!IsSimple(prop.PropertyType))
|
||||
serializer.Serialize(writer, prop.GetValue(value));
|
||||
else
|
||||
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
}
|
||||
|
||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
|
||||
|
||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
/// </returns>
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var value = reader.Value?.ToString().ToLower().Trim();
|
||||
var value = reader.Value?.ToString()!.ToLower().Trim();
|
||||
if (value == null || value == "")
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
|
||||
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
|
||||
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
||||
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
|
||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
||||
: new MemoryStream(data.ToArray());
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
|
||||
using var jsonTextReader = new JsonTextReader(reader);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Node accessor
|
||||
/// </summary>
|
||||
public struct NodeAccessor
|
||||
public readonly struct NodeAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Index
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
/// <summary>
|
||||
/// Message access definition
|
||||
/// </summary>
|
||||
public struct MessagePath : IEnumerable<NodeAccessor>
|
||||
public readonly struct MessagePath : IEnumerable<NodeAccessor>
|
||||
{
|
||||
private List<NodeAccessor> _path;
|
||||
private readonly List<NodeAccessor> _path;
|
||||
|
||||
internal void Add(NodeAccessor node)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class ArrayPropertyInfo
|
||||
@@ -32,17 +32,77 @@ 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.TargetType == typeof(string))
|
||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
else if(prop.TargetType.IsEnum)
|
||||
writer.WriteStringValue(EnumConverter.GetString(objValue));
|
||||
else if (prop.TargetType == typeof(bool))
|
||||
writer.WriteBooleanValue((bool)objValue);
|
||||
else
|
||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -51,8 +111,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return default;
|
||||
|
||||
var result = Activator.CreateInstance(typeToConvert);
|
||||
return (T)ParseObject(ref reader, result, typeToConvert);
|
||||
var result = Activator.CreateInstance(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 +143,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 +152,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 +166,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(attribute.PropertyInfo.PropertyType, newOptions);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
// Use default deserialization
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters);
|
||||
}
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class BoolConverterInner<T> : JsonConverter<T>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for comma seperated enum values
|
||||
/// </summary>
|
||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
@@ -28,7 +27,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
||||
@@ -48,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var longValue = reader.GetDouble();
|
||||
if (longValue == 0 || longValue == -1)
|
||||
if (longValue == 0 || longValue < 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDouble(longValue);
|
||||
@@ -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;
|
||||
@@ -74,7 +74,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
var dtValue = (DateTime)(object)value;
|
||||
@@ -154,6 +156,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,33 @@ 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, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
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 />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter mapping to an object but also handles when an empty array is send
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
|
||||
{
|
||||
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T? Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.StartArray:
|
||||
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
|
||||
return default;
|
||||
case JsonTokenType.StartObject:
|
||||
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
|
||||
};
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
=> JsonSerializer.Serialize(writer, (object?)value, options);
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return true;
|
||||
}
|
||||
|
||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||
{
|
||||
var intValue = int.Parse(value);
|
||||
result = Enum.ToObject(objectType, intValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
@@ -211,5 +218,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,31 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute for allowing specifying a JsonConverter with constructor parameters
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class JsonConverterCtorAttribute : JsonConverterAttribute
|
||||
{
|
||||
private readonly object[] _parameters;
|
||||
private readonly Type _type;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public JsonConverterCtorAttribute(Type type, params object[] parameters)
|
||||
{
|
||||
_type = type;
|
||||
_parameters = parameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
||||
{
|
||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Replace a value on a string property
|
||||
/// </summary>
|
||||
public class ReplaceConverter : JsonConverter<string>
|
||||
{
|
||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ReplaceConverter(params string[] replaceSets)
|
||||
{
|
||||
_replacementSets = replaceSets.Select(x =>
|
||||
{
|
||||
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
||||
if (split.Length != 2)
|
||||
throw new ArgumentException("Invalid replacement config");
|
||||
return (split[0], split[1]);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
foreach (var set in _replacementSets)
|
||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
new EnumConverter(),
|
||||
new BoolConverter(),
|
||||
new DecimalConverter(),
|
||||
new IntConverter(),
|
||||
new LongConverter()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// </summary>
|
||||
protected JsonDocument? _document;
|
||||
|
||||
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
||||
private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsJson { get; set; }
|
||||
@@ -31,6 +32,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonMessageAccessor()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
||||
{
|
||||
_customSerializerOptions = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
@@ -42,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
try
|
||||
{
|
||||
var result = _document.Deserialize(type, _serializerOptions);
|
||||
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
|
||||
return new CallResult<object>(result!);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
@@ -50,6 +66,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 />
|
||||
@@ -60,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
try
|
||||
{
|
||||
var result = _document.Deserialize<T>(_serializerOptions);
|
||||
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
||||
return new CallResult<T>(result!);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
@@ -68,6 +89,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 +142,40 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value.Value.Deserialize<T>(_customSerializerOptions ?? _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)
|
||||
{
|
||||
@@ -187,6 +240,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonStreamMessageAccessor(): base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||
{
|
||||
@@ -237,6 +304,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
|
||||
@@ -249,6 +317,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonByteMessageAccessor() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
@@ -256,6 +338,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);
|
||||
@@ -270,7 +360,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString() =>
|
||||
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
#if NETSTANDARD2_0
|
||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
||||
#else
|
||||
@@ -284,6 +374,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<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.8.0</PackageVersion>
|
||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
||||
<FileVersion>8.8.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>
|
||||
@@ -20,7 +20,7 @@
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@@ -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>
|
||||
@@ -48,16 +48,17 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<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="9.0.0" />
|
||||
<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="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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>
|
||||
@@ -132,27 +160,16 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
||||
/// </summary>
|
||||
/// <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
|
||||
@@ -163,7 +180,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
var randomChars = new char[length];
|
||||
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||
#else
|
||||
@@ -191,5 +208,71 @@ 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.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -66,7 +67,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
||||
}
|
||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||
{
|
||||
@@ -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;
|
||||
@@ -107,93 +111,8 @@ namespace CryptoExchange.Net
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||
}
|
||||
}
|
||||
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;
|
||||
return formData.ToString()!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -271,6 +190,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 +244,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>
|
||||
@@ -378,6 +287,7 @@ namespace CryptoExchange.Net
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
@@ -425,6 +335,7 @@ namespace CryptoExchange.Net
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
@@ -436,7 +347,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
||||
{
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
@@ -450,7 +361,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using Gzip
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
@@ -458,12 +369,141 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
using var decompressedStream = new MemoryStream();
|
||||
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
||||
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
|
||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
||||
: new MemoryStream(data.ToArray());
|
||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||
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(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFeeRestClient)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,8 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -12,13 +16,20 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||
/// </summary>
|
||||
bool Authenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Format a base and quote asset to an exchange accepted symbol
|
||||
/// </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
|
||||
@@ -26,5 +37,12 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="credentials"></param>
|
||||
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
|
||||
|
||||
/// <summary>
|
||||
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Api credentials type</typeparam>
|
||||
/// <param name="options">Options to set</param>
|
||||
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
/// <param name="httpClient">Optional shared http client instance</param>
|
||||
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
|
||||
|
||||
/// <summary>
|
||||
/// Update settings
|
||||
/// </summary>
|
||||
/// <param name="proxy">Proxy to use</param>
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +19,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
int CurrentSubscriptions { get; }
|
||||
/// <summary>
|
||||
/// Incoming data kpbs
|
||||
/// Incoming data Kbps
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <summary>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
|
||||
/// Get the average price that a market order would fill at at the current order book state. This is no guarantee that an order of that quantity would actually be filled
|
||||
/// at that price since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quantity">The quantity in base asset to fill</param>
|
||||
@@ -115,7 +115,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
|
||||
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
|
||||
/// This is no guarantee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
|
||||
/// <param name="type">The type</param>
|
||||
|
||||
@@ -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;
|
||||
@@ -43,7 +47,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
event Func<Task> OnReconnected;
|
||||
/// <summary>
|
||||
/// Get reconntion url
|
||||
/// Get reconnection url
|
||||
/// </summary>
|
||||
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
@@ -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>
|
||||
@@ -89,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task CloseAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Update proxy setting
|
||||
/// </summary>
|
||||
void UpdateProxy(ApiProxy? proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for client libraries
|
||||
/// </summary>
|
||||
public static class LibraryHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Client order id separator
|
||||
/// </summary>
|
||||
public const string ClientOrderIdSeparator = "JK";
|
||||
|
||||
/// <summary>
|
||||
/// Apply broker id to a client order id
|
||||
/// </summary>
|
||||
/// <param name="clientOrderId"></param>
|
||||
/// <param name="brokerId"></param>
|
||||
/// <param name="maxLength"></param>
|
||||
/// <param name="allowValueAdjustment"></param>
|
||||
/// <returns></returns>
|
||||
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustment)
|
||||
{
|
||||
var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length;
|
||||
|
||||
if ((clientOrderId?.Length + reservedLength) > maxLength)
|
||||
return clientOrderId!;
|
||||
|
||||
if (!string.IsNullOrEmpty(clientOrderId))
|
||||
{
|
||||
if (allowValueAdjustment)
|
||||
clientOrderId = brokerId + ClientOrderIdSeparator + clientOrderId;
|
||||
|
||||
return clientOrderId!;
|
||||
}
|
||||
else
|
||||
{
|
||||
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeparator, maxLength);
|
||||
}
|
||||
|
||||
return clientOrderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-6
@@ -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;
|
||||
@@ -31,7 +33,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, Exception?> _receiveLoopStoppedWithException;
|
||||
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, TimeSpan?, Exception?> _noDataReceiveTimeoutReconnect;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
|
||||
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
|
||||
|
||||
static CryptoExchangeWebSocketClientLoggingExtension()
|
||||
{
|
||||
@@ -151,7 +155,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");
|
||||
|
||||
@@ -165,10 +169,26 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(1026, "StartingTaskForNoDataReceivedCheck"),
|
||||
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
|
||||
|
||||
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
|
||||
LogLevel.Debug,
|
||||
_noDataReceiveTimeoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
|
||||
LogLevel.Warning,
|
||||
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(1029, "SocketProcessingStateChanged"),
|
||||
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
|
||||
|
||||
_socketPingTimeout = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
new EventId(1030, "SocketPingTimeout"),
|
||||
"[Sckt {Id}] ping frame timeout; reconnecting socket");
|
||||
|
||||
}
|
||||
|
||||
public static void SocketConnecting(
|
||||
@@ -285,6 +305,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)
|
||||
{
|
||||
@@ -330,7 +356,19 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
public static void SocketNoDataReceiveTimoutReconnect(
|
||||
this ILogger logger, int socketId, TimeSpan? timeSpan)
|
||||
{
|
||||
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
|
||||
_noDataReceiveTimeoutReconnect(logger, socketId, timeSpan, null);
|
||||
}
|
||||
|
||||
public static void SocketProcessingStateChanged(
|
||||
this ILogger logger, int socketId, string prevState, string newState)
|
||||
{
|
||||
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
|
||||
}
|
||||
|
||||
public static void SocketPingTimeout(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_socketPingTimeout(logger, socketId, 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,7 +18,10 @@ 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;
|
||||
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
|
||||
|
||||
static RestApiClientLoggingExtensions()
|
||||
{
|
||||
@@ -33,7 +37,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
|
||||
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4002, "RestApifailedToSyncTime"),
|
||||
new EventId(4002, "RestApiFailedToSyncTime"),
|
||||
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
|
||||
|
||||
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
|
||||
@@ -65,6 +69,27 @@ 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}");
|
||||
|
||||
_restApiCancellationRequested = LoggerMessage.Define<int?>(
|
||||
LogLevel.Debug,
|
||||
new EventId(4012, "RestApiCancellationRequested"),
|
||||
"[Req {RequestId}] Request cancelled by user");
|
||||
|
||||
}
|
||||
|
||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
||||
@@ -111,5 +136,24 @@ 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);
|
||||
}
|
||||
public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
|
||||
{
|
||||
_restApiCancellationRequested(logger, requestId, 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,12 +4,13 @@ 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;
|
||||
private static readonly Action<ILogger, int, string?, Exception?> _failedReconnectProcessing;
|
||||
private static readonly Action<ILogger, int, Exception?> _unkownExceptionWhileProcessingReconnection;
|
||||
private static readonly Action<ILogger, int, Exception?> _unknownExceptionWhileProcessingReconnection;
|
||||
private static readonly Action<ILogger, int, WebSocketError, string?, Exception?> _webSocketErrorCodeAndDetails;
|
||||
private static readonly Action<ILogger, int, string?, Exception?> _webSocketError;
|
||||
private static readonly Action<ILogger, int, int, Exception?> _messageSentNotPending;
|
||||
@@ -27,7 +28,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, Exception?> _closingNoMoreSubscriptions;
|
||||
private static readonly Action<ILogger, int, int, int, Exception?> _addingNewSubscription;
|
||||
private static readonly Action<ILogger, int, Exception?> _nothingToResubscribeCloseConnection;
|
||||
private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndRecoonect;
|
||||
private static readonly Action<ILogger, int, Exception?> _failedAuthenticationDisconnectAndReconnect;
|
||||
private static readonly Action<ILogger, int, Exception?> _authenticationSucceeded;
|
||||
private static readonly Action<ILogger, int, string?, Exception?> _failedRequestRevitalization;
|
||||
private static readonly Action<ILogger, int, Exception?> _allSubscriptionResubscribed;
|
||||
@@ -54,15 +55,15 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(2002, "FailedReconnectProcessing"),
|
||||
"[Sckt {SocketId}] failed reconnect processing: {ErrorMessage}, reconnecting again");
|
||||
|
||||
_unkownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
|
||||
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2003, "UnkownExceptionWhileProcessingReconnection"),
|
||||
new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
|
||||
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
|
||||
|
||||
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2004, "WebSocketErrorCode"),
|
||||
"[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCdoe}, details: {Details}");
|
||||
"[Sckt {SocketId}] error: Websocket error code {WebSocketErrorCode}, details: {Details}");
|
||||
|
||||
_webSocketError = LoggerMessage.Define<int, string?>(
|
||||
LogLevel.Warning,
|
||||
@@ -144,7 +145,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(2020, "NothingToResubscribe"),
|
||||
"[Sckt {SocketId}] nothing to resubscribe, closing connection");
|
||||
|
||||
_failedAuthenticationDisconnectAndRecoonect = LoggerMessage.Define<int>(
|
||||
_failedAuthenticationDisconnectAndReconnect = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2021, "FailedAuthentication"),
|
||||
"[Sckt {SocketId}] authentication failed on reconnected socket. Disconnecting and reconnecting");
|
||||
@@ -182,7 +183,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_sendingData = LoggerMessage.Define<int, int, string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(2028, "SendingData"),
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}");
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
|
||||
|
||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Warning,
|
||||
@@ -205,9 +206,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_failedReconnectProcessing(logger, socketId, error, null);
|
||||
}
|
||||
|
||||
public static void UnkownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e)
|
||||
public static void UnknownExceptionWhileProcessingReconnection(this ILogger logger, int socketId, Exception e)
|
||||
{
|
||||
_unkownExceptionWhileProcessingReconnection(logger, socketId, e);
|
||||
_unknownExceptionWhileProcessingReconnection(logger, socketId, e);
|
||||
}
|
||||
|
||||
public static void WebSocketErrorCodeAndDetails(this ILogger logger, int socketId, WebSocketError error, string? details, Exception e)
|
||||
@@ -284,7 +285,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
}
|
||||
public static void FailedAuthenticationDisconnectAndRecoonect(this ILogger logger, int socketId)
|
||||
{
|
||||
_failedAuthenticationDisconnectAndRecoonect(logger, socketId, null);
|
||||
_failedAuthenticationDisconnectAndReconnect(logger, socketId, null);
|
||||
}
|
||||
public static void AuthenticationSucceeded(this ILogger logger, int socketId)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
@@ -60,7 +62,6 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(5005, "OrderBookStopping"),
|
||||
"{Api} order book {Symbol} stopping");
|
||||
|
||||
|
||||
_orderBookStopped = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Trace,
|
||||
new EventId(5006, "OrderBookStopped"),
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy info
|
||||
@@ -10,30 +8,21 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The host address of the proxy
|
||||
/// </summary>
|
||||
public string Host { get; }
|
||||
public string Host { get; set; }
|
||||
/// <summary>
|
||||
/// The port of the proxy
|
||||
/// </summary>
|
||||
public int Port { get; }
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The login of the proxy
|
||||
/// </summary>
|
||||
public string? Login { get; }
|
||||
public string? Login { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password of the proxy
|
||||
/// </summary>
|
||||
public SecureString? Password { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
/// </summary>
|
||||
/// <param name="host">The proxy hostname/ip</param>
|
||||
/// <param name="port">The proxy port</param>
|
||||
public ApiProxy(string host, int port): this(host, port, null, (SecureString?)null)
|
||||
{
|
||||
}
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
@@ -42,18 +31,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 = null, string? password = null)
|
||||
{
|
||||
Host = host;
|
||||
Port = port;
|
||||
|
||||
@@ -32,23 +32,30 @@ namespace CryptoExchange.Net.Objects
|
||||
/// Wait for the AutoResetEvent to be set
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task<bool> WaitAsync(TimeSpan? timeout = null)
|
||||
public async Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
||||
{
|
||||
lock (_waits)
|
||||
CancellationTokenRegistration registration = default;
|
||||
try
|
||||
{
|
||||
if (_signaled)
|
||||
Task<bool> waiter = _completed;
|
||||
lock (_waits)
|
||||
{
|
||||
if(_reset)
|
||||
_signaled = false;
|
||||
return _completed;
|
||||
}
|
||||
else
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if(timeout != null)
|
||||
if (_signaled)
|
||||
{
|
||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
||||
var registration = cancellationSource.Token.Register(() =>
|
||||
if (_reset)
|
||||
_signaled = false;
|
||||
}
|
||||
else if (!ct.IsCancellationRequested)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
lock (_waits)
|
||||
{
|
||||
@@ -58,11 +65,18 @@ namespace CryptoExchange.Net.Objects
|
||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||
}
|
||||
}, useSynchronizationContext: false);
|
||||
}
|
||||
|
||||
_waits.Enqueue(tcs);
|
||||
return tcs.Task;
|
||||
|
||||
_waits.Enqueue(tcs);
|
||||
waiter = tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
return await waiter.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
registration.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <returns></returns>
|
||||
public int Compare(byte[] x, byte[] y)
|
||||
public int Compare(byte[]? x, byte[]? y)
|
||||
{
|
||||
// Shortcuts: If both are null, they are the same.
|
||||
if (x == null && y == null) return 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
@@ -88,7 +89,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The erro rto return</param>
|
||||
/// <param name="error">The error to return</param>
|
||||
public CallResult(Error error) : this(default, null, error) { }
|
||||
|
||||
/// <summary>
|
||||
@@ -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 />
|
||||
|
||||
@@ -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_";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace CryptoExchange.Net.Objects
|
||||
using CryptoExchange.Net.Attributes;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// What to do when a request would exceed the rate limit
|
||||
@@ -68,6 +70,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>
|
||||
Disposed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of the order book
|
||||
/// </summary>
|
||||
@@ -152,7 +181,11 @@
|
||||
/// <summary>
|
||||
/// Round to closest value
|
||||
/// </summary>
|
||||
Closest
|
||||
Closest,
|
||||
/// <summary>
|
||||
/// Round up (ceil)
|
||||
/// </summary>
|
||||
Up
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,4 +202,53 @@
|
||||
/// </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>
|
||||
/// Backoff 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of exchange
|
||||
/// </summary>
|
||||
public enum ExchangeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized
|
||||
/// </summary>
|
||||
CEX,
|
||||
/// <summary>
|
||||
/// Decentralized
|
||||
/// </summary>
|
||||
DEX
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Library options
|
||||
/// </summary>
|
||||
/// <typeparam name="TRestOptions"></typeparam>
|
||||
/// <typeparam name="TSocketOptions"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
where TRestOptions: RestExchangeOptions, new()
|
||||
where TSocketOptions: SocketExchangeOptions, new()
|
||||
where TApiCredentials: ApiCredentials
|
||||
where TEnvironment: TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Rest client options
|
||||
/// </summary>
|
||||
public TRestOptions Rest { get; set; } = new TRestOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Socket client options
|
||||
/// </summary>
|
||||
public TSocketOptions Socket { get; set; } = new TSocketOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API.
|
||||
/// </summary>
|
||||
public TEnvironment? Environment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests.
|
||||
/// </summary>
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The DI service lifetime for the socket client
|
||||
/// </summary>
|
||||
public ServiceLifetime? SocketClientLifeTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy values from these options to the target options
|
||||
/// </summary>
|
||||
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
{
|
||||
targetOptions.ApiCredentials = ApiCredentials;
|
||||
targetOptions.Environment = Environment;
|
||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||
targetOptions.Socket = Socket.Set(targetOptions.Socket);
|
||||
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public TimeSpan? TimestampRecalculationInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public virtual T Copy<T>() where T : RestApiOptions, new()
|
||||
public T Set<T>(T item) where T : RestApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,23 +19,31 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// 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>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestExchangeOptions, new()
|
||||
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>
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
public T Set<T>(T item) where T : RestExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour
|
||||
};
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.CachingEnabled = CachingEnabled;
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,22 +55,20 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
|
||||
public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
base.Set(target);
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketApiOptions, new()
|
||||
public T Set<T>(T item) where T : SocketApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,29 +46,43 @@ 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>
|
||||
/// The buffer size to use for receiving data. Leave unset to use the default buffer size.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only specify this if you are creating a significant amount of connections and understand the typical message length we receive from the exchange.
|
||||
/// Setting this too low can increase memory consumption and allocations.
|
||||
/// </remarks>
|
||||
public int? ReceiveBufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketExchangeOptions, new()
|
||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoReconnect = AutoReconnect,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.ReconnectPolicy = ReconnectPolicy;
|
||||
item.DelayAfterConnect = DelayAfterConnect;
|
||||
item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
|
||||
item.ReconnectInterval = ReconnectInterval;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,15 +101,13 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
base.Set(target);
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options to update
|
||||
/// </summary>
|
||||
public class UpdateOptions<T> where T : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
/// <summary>
|
||||
/// Api credentials
|
||||
/// </summary>
|
||||
public T? ApiCredentials { get; set; }
|
||||
/// <summary>
|
||||
/// Request timeout
|
||||
/// </summary>
|
||||
public TimeSpan? RequestTimeout { get; set; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <returns></returns>
|
||||
public int Compare(string x, string y)
|
||||
public int Compare(string? x, string? y)
|
||||
{
|
||||
// Shortcuts: If both are null, they are the same.
|
||||
if (x == null && y == null) return 0;
|
||||
|
||||
@@ -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>
|
||||
@@ -165,8 +187,8 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="value"></param>
|
||||
public void AddEnumAsInt<T>(string key, T value)
|
||||
{
|
||||
var stringVal = EnumConverter.GetString(value);
|
||||
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
|
||||
var stringVal = EnumConverter.GetString(value)!;
|
||||
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
|
||||
@@ -26,8 +25,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
|
||||
|
||||
// Formating
|
||||
// Formatting
|
||||
|
||||
/// <summary>
|
||||
/// The body format for this request
|
||||
@@ -48,18 +46,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,27 +53,56 @@ 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)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
|
||||
|
||||
/// <summary>
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
/// </summary>
|
||||
/// <param name="identifier">Request identifier</param>
|
||||
/// <param name="method">The HttpMethod</param>
|
||||
/// <param name="path">Endpoint path</param>
|
||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
|
||||
/// <param name="weight">Request weight</param>
|
||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||
/// <param name="requestBodyFormat">Request body format</param>
|
||||
/// <param name="parameterPosition">Parameter position</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
string identifier,
|
||||
HttpMethod method,
|
||||
string path,
|
||||
IRateLimitGate? rateLimitGate,
|
||||
int weight,
|
||||
bool authenticated,
|
||||
IRateLimitGuard? limitGuard = null,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null)
|
||||
{
|
||||
|
||||
if (!_definitions.TryGetValue(method + path, out var def))
|
||||
if (!_definitions.TryGetValue(identifier, out var def))
|
||||
{
|
||||
def = new RequestDefinition(path, method)
|
||||
{
|
||||
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);
|
||||
_definitions.TryAdd(identifier, def);
|
||||
}
|
||||
|
||||
return def;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
@@ -11,12 +12,22 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// The timestamp the data was received
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
public DateTime ReceiveTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The topic of the update, what symbol/asset etc..
|
||||
/// The timestamp of the data as specified by the server. Note that the server time and client time might not be 100% in sync so this value might not be fully comparable to local time.
|
||||
/// </summary>
|
||||
public string? Topic { get; set; }
|
||||
public DateTime? DataTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The stream producing the update
|
||||
/// </summary>
|
||||
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,49 +44,153 @@ 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 receiveTimestamp, SocketUpdateType? updateType)
|
||||
{
|
||||
Data = data;
|
||||
Topic = topic;
|
||||
StreamId = streamId;
|
||||
Symbol = symbol;
|
||||
OriginalData = originalData;
|
||||
Timestamp = timestamp;
|
||||
ReceiveTime = receiveTimestamp;
|
||||
UpdateType = updateType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and Timestamp will be copied over
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <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, ReceiveTime, UpdateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <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, ReceiveTime, UpdateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and ReceivedTimestamp will be copied over
|
||||
/// </summary>
|
||||
/// <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, ReceiveTime, updateType)
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
DataTime = DataTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Specify the data timestamp
|
||||
/// </summary>
|
||||
public DataEvent<T> WithDataTimestamp(DateTime? timestamp)
|
||||
{
|
||||
DataTime = timestamp;
|
||||
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,9 +30,18 @@ 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
|
||||
/// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the disconnect
|
||||
/// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes.
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored
|
||||
|
||||
@@ -26,22 +26,22 @@ 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
|
||||
/// The maximum time of no data received before considering the connection lost and closing/reconnecting the socket
|
||||
/// </summary>
|
||||
public TimeSpan? Timeout { get; set; }
|
||||
|
||||
@@ -57,22 +57,27 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <summary>
|
||||
/// What to do when rate limit is reached
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; }
|
||||
public RateLimitingBehaviour RateLimitingBehavior { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Encoding for sending/receiving data
|
||||
/// </summary>
|
||||
public Encoding Encoding { get; set; } = Encoding.UTF8;
|
||||
|
||||
/// <summary>
|
||||
/// The buffer size to use for receiving data
|
||||
/// </summary>
|
||||
public int? ReceiveBufferSize { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
|
||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
||||
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
||||
{
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
|
||||
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
|
||||
TimeSyncState.TimeOffset = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
|
||||
TimeSyncState.TimeOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,21 @@
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the echange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
public class TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the environment
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; init; }
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
protected TradeEnvironment(string name)
|
||||
{
|
||||
EnvironmentName = name;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +74,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
/// <summary>
|
||||
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
|
||||
/// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the
|
||||
/// server handle this
|
||||
/// the current level 25 ask out of the top 25, should the level 26 entry be removed from the book or does the server handle this
|
||||
/// </summary>
|
||||
protected bool _strictLevels;
|
||||
|
||||
@@ -250,6 +249,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
// Clear any previous messages
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
|
||||
_processBuffer.Clear();
|
||||
_bookSet = false;
|
||||
|
||||
@@ -407,7 +407,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
/// <summary>
|
||||
/// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot
|
||||
/// received from a socket subcription
|
||||
/// received from a socket subscription
|
||||
/// </summary>
|
||||
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
|
||||
/// <param name="askList">List of asks</param>
|
||||
@@ -618,6 +618,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var bid = book.bids.Count() > i ? book.bids.ElementAt(i): null;
|
||||
stringBuilder.AppendLine($"[{ask?.Quantity.ToString(CultureInfo.InvariantCulture),14}] {ask?.Price.ToString(CultureInfo.InvariantCulture),14} | {bid?.Price.ToString(CultureInfo.InvariantCulture),-14} [{bid?.Quantity.ToString(CultureInfo.InvariantCulture),-14}]");
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
@@ -636,6 +637,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_queueEvent.Set();
|
||||
// Clear queue
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
|
||||
_processBuffer.Clear();
|
||||
_bookSet = false;
|
||||
DoReset();
|
||||
@@ -732,7 +734,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var (prevBestBid, prevBestAsk) = BestOffers;
|
||||
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
|
||||
|
||||
if (!_asks.Any() || !_bids.Any())
|
||||
if (_asks.Count == 0 || _bids.Count == 0)
|
||||
return;
|
||||
|
||||
if (_asks.First().Key < _bids.First().Key)
|
||||
@@ -810,7 +812,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (lastUpdateId <= LastSequenceNumber)
|
||||
{
|
||||
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
|
||||
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -843,9 +845,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
internal class DescComparer<T> : IComparer<T>
|
||||
{
|
||||
public int Compare(T x, T y)
|
||||
public int Compare(T? x, T? y)
|
||||
{
|
||||
return Comparer<T>.Default.Compare(y, x);
|
||||
return Comparer<T>.Default.Compare(y!, x!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user