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

Compare commits

..

1 Commits

Author SHA1 Message Date
JKorf decef7b137 Update CryptoExchange.Net.csproj 2024-07-03 21:53:36 +02:00
292 changed files with 1331 additions and 13820 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 9.0.x dotnet-version: 6.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference> <PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,7 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -71,20 +70,5 @@ namespace CryptoExchange.Net.UnitTests
var result = ExchangeHelpers.Normalize(input); var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture)); Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
} }
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
} }
} }
+11 -27
View File
@@ -51,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
// assert // assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10)); Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key == "123"); Assert.That(options.ApiCredentials.Key.GetString() == "123");
Assert.That(options.ApiCredentials.Secret == "456"); Assert.That(options.ApiCredentials.Secret.GetString() == "456");
} }
[Test] [Test]
@@ -64,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101"); options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert // assert
Assert.That(options.Api1Options.ApiCredentials.Key == "123"); Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret == "456"); Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456");
Assert.That(options.Api2Options.ApiCredentials.Key == "789"); Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret == "101"); Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101");
} }
[Test] [Test]
@@ -100,10 +100,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider1.GetSecret() == "222"); Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123"); Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
[Test] [Test]
@@ -125,10 +121,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider2.GetKey() == "123"); Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123"); Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
} }
@@ -142,14 +134,6 @@ namespace CryptoExchange.Net.UnitTests
Environment = new TestEnvironment("test", "https://test.com") Environment = new TestEnvironment("test", "https://test.com")
}; };
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary> /// <summary>
/// The default receive window for requests /// The default receive window for requests
/// </summary> /// </summary>
@@ -159,12 +143,12 @@ namespace CryptoExchange.Net.UnitTests
public RestApiOptions Api2Options { get; set; } = new RestApiOptions(); public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Set(TestClientOptions targetOptions) internal TestClientOptions Copy()
{ {
targetOptions = base.Set<TestClientOptions>(targetOptions); var options = Copy<TestClientOptions>();
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options); options.Api1Options = Api1Options.Copy<RestApiOptions>();
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options); options.Api2Options = Api2Options.Copy<RestApiOptions>();
return targetOptions; return options;
} }
} }
} }
+17 -32
View File
@@ -176,12 +176,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) for (var i = 0; i < requests + 1; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(i == requests? triggered : !triggered); Assert.That(i == requests? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@@ -201,7 +201,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimiting ? evnt != null : evnt == null); Assert.That(expectLimiting ? evnt != null : evnt == null);
} }
@@ -243,12 +243,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) for (var i = 0; i < requests + 1; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(i == requests ? triggered : !triggered); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@@ -266,7 +266,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -302,16 +302,16 @@ namespace CryptoExchange.Net.UnitTests
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited) public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
{ {
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null }; var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null }; var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -328,7 +328,7 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
@@ -348,9 +348,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -365,25 +365,10 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent evnt = null; RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
[Test]
public async Task ConnectionRateLimiterCancel()
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
} }
} }
@@ -58,7 +58,9 @@ namespace CryptoExchange.Net.UnitTests
options.ReconnectInterval = TimeSpan.Zero; options.ReconnectInterval = TimeSpan.Zero;
}); });
var socket = client.CreateSocket(); var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true; socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false); var rstEvent = new ManualResetEvent(false);
Dictionary<string, string> result = null; Dictionary<string, string> result = null;
@@ -73,7 +75,7 @@ namespace CryptoExchange.Net.UnitTests
sub.AddSubscription(subObj); sub.AddSubscription(subObj);
// act // act
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}"); socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000); rstEvent.WaitOne(1000);
// assert // assert
@@ -91,7 +93,9 @@ namespace CryptoExchange.Net.UnitTests
options.SubOptions.OutputOriginalData = enabled; options.SubOptions.OutputOriginalData = enabled;
}); });
var socket = client.CreateSocket(); var socket = client.CreateSocket();
socket.ShouldReconnect = true;
socket.CanConnect = true; socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false); var rstEvent = new ManualResetEvent(false);
string original = null; string original = null;
@@ -103,7 +107,7 @@ namespace CryptoExchange.Net.UnitTests
rstEvent.Set(); rstEvent.Set();
}); });
sub.AddSubscription(subObj); sub.AddSubscription(subObj);
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 }); var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
// act // act
socket.InvokeMessage(msgToSend); socket.InvokeMessage(msgToSend);
@@ -198,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
// act // act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" })); socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" }));
await sub; await sub;
// assert // assert
@@ -221,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
// act // act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" })); socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" }));
await sub; await sub;
// assert // assert
@@ -5,8 +5,6 @@ using NUnit.Framework;
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
@@ -165,20 +163,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(output.Value == expected); Assert.That(output.Value == expected);
} }
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
Assert.That(result == expected);
}
[TestCase("1", true)] [TestCase("1", true)]
[TestCase("true", true)] [TestCase("true", true)]
[TestCase("yes", true)] [TestCase("yes", true)]
@@ -216,79 +200,6 @@ namespace CryptoExchange.Net.UnitTests
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}"); var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected); Assert.That(output.Value == expected);
} }
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterNumber(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[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 public class STJTimeObject
@@ -321,42 +232,4 @@ namespace CryptoExchange.Net.UnitTests
[JsonConverter(typeof(BoolConverter))] [JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; } 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,10 +10,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{ {
internal class SubResponse internal class SubResponse
{ {
[JsonProperty("action")]
public string Action { get; set; } = null!;
[JsonProperty("channel")] [JsonProperty("channel")]
public string Channel { get; set; } = null!; public string Channel { get; set; } = null!;
@@ -23,9 +19,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
internal class UnsubResponse internal class UnsubResponse
{ {
[JsonProperty("action")]
public string Action { get; set; } = null!;
[JsonProperty("status")] [JsonProperty("status")]
public string Status { get; set; } = null!; public string Status { get; set; } = null!;
} }
@@ -36,7 +29,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight) public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{ {
ListenerIdentifiers = new HashSet<string> { request + "-" + channel }; ListenerIdentifiers = new HashSet<string> { channel };
} }
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message) 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; private readonly Action<DataEvent<T>> _handler;
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" }; public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false) public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{ {
@@ -8,7 +8,6 @@ using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -20,7 +19,7 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test") public TestBaseClient(): base(null, "Test")
{ {
var options = new TestClientOptions(); var options = TestClientOptions.Default.Copy();
Initialize(options); Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions())); SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
} }
@@ -56,7 +55,7 @@ namespace CryptoExchange.Net.UnitTests
} }
/// <inheritdoc /> /// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null; public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null; public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException(); protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
@@ -69,11 +68,11 @@ namespace CryptoExchange.Net.UnitTests
{ {
} }
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 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 string GetKey() => _credentials.Key; public string GetKey() => _credentials.Key.GetString();
public string GetSecret() => _credentials.Secret; public string GetSecret() => _credentials.Secret.GetString();
} }
} }
@@ -15,8 +15,6 @@ using System.Collections.Generic;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -25,17 +23,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; } public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; } public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsDelegate = null) public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{ {
} }
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test") public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
{ {
Initialize(options.Value); }
Api1 = new TestRestApi1Client(options.Value); public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
Api2 = new TestRestApi2Client(options.Value); {
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
} }
public void SetResponse(string responseData, out IRequest requestObj) public void SetResponse(string responseData, out IRequest requestObj)
@@ -135,7 +138,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
} }
/// <inheritdoc /> /// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{ {
@@ -179,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
} }
/// <inheritdoc /> /// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{ {
@@ -1,132 +1,131 @@
//using System; using System;
//using System.IO; using System.IO;
//using System.Net.WebSockets; using System.Net.WebSockets;
//using System.Security.Authentication; using System.Security.Authentication;
//using System.Text; using System.Text;
//using System.Threading.Tasks; using System.Threading.Tasks;
//using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
//using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
//namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
//{ {
// public class TestSocket: IWebsocket public class TestSocket: IWebsocket
// { {
// public bool CanConnect { get; set; } public bool CanConnect { get; set; }
// public bool Connected { get; set; } public bool Connected { get; set; }
// public event Func<Task> OnClose; public event Func<Task> OnClose;
//#pragma warning disable 0067 #pragma warning disable 0067
// public event Func<Task> OnReconnected; public event Func<Task> OnReconnected;
// public event Func<Task> OnReconnecting; public event Func<Task> OnReconnecting;
// public event Func<int, Task> OnRequestRateLimited; public event Func<int, Task> OnRequestRateLimited;
//#pragma warning restore 0067 #pragma warning restore 0067
// public event Func<int, Task> OnRequestSent; public event Func<int, Task> OnRequestSent;
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage; public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
// public event Func<Exception, Task> OnError; public event Func<Exception, Task> OnError;
// public event Func<Task> OnOpen; public event Func<Task> OnOpen;
// public Func<Task<Uri>> GetReconnectionUrl { get; set; } public Func<Task<Uri>> GetReconnectionUrl { get; set; }
// public int Id { get; } public int Id { get; }
// public bool ShouldReconnect { get; set; } public bool ShouldReconnect { get; set; }
// public TimeSpan Timeout { get; set; } public TimeSpan Timeout { get; set; }
// public Func<string, string> DataInterpreterString { get; set; } public Func<string, string> DataInterpreterString { get; set; }
// public Func<byte[], string> DataInterpreterBytes { get; set; } public Func<byte[], string> DataInterpreterBytes { get; set; }
// public DateTime? DisconnectTime { get; set; } public DateTime? DisconnectTime { get; set; }
// public string Url { get; } public string Url { get; }
// public bool IsClosed => !Connected; public bool IsClosed => !Connected;
// public bool IsOpen => Connected; public bool IsOpen => Connected;
// public bool PingConnection { get; set; } public bool PingConnection { get; set; }
// public TimeSpan PingInterval { get; set; } public TimeSpan PingInterval { get; set; }
// public SslProtocols SSLProtocols { get; set; } public SslProtocols SSLProtocols { get; set; }
// public Encoding Encoding { get; set; } public Encoding Encoding { get; set; }
// public int ConnectCalls { get; private set; } public int ConnectCalls { get; private set; }
// public bool Reconnecting { get; set; } public bool Reconnecting { get; set; }
// public string Origin { get; set; } public string Origin { get; set; }
// public int? RatelimitPerSecond { 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 int lastId = 0;
// public static object lastIdLock = new object(); public static object lastIdLock = new object();
// public TestSocket() public TestSocket()
// { {
// lock (lastIdLock) lock (lastIdLock)
// { {
// Id = lastId + 1; Id = lastId + 1;
// lastId++; lastId++;
// } }
// } }
// public Task<CallResult> ConnectAsync() public Task<CallResult> ConnectAsync()
// { {
// Connected = CanConnect; Connected = CanConnect;
// ConnectCalls++; ConnectCalls++;
// if (CanConnect) if (CanConnect)
// InvokeOpen(); InvokeOpen();
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError())); return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
// } }
// public bool Send(int requestId, string data, int weight) public void Send(int requestId, string data, int weight)
// { {
// if(!Connected) if(!Connected)
// throw new Exception("Socket not connected"); throw new Exception("Socket not connected");
// OnRequestSent?.Invoke(requestId); OnRequestSent?.Invoke(requestId);
// return true; }
// }
// public void Reset() public void Reset()
// { {
// } }
// public Task CloseAsync() public Task CloseAsync()
// { {
// Connected = false; Connected = false;
// DisconnectTime = DateTime.UtcNow; DisconnectTime = DateTime.UtcNow;
// OnClose?.Invoke(); OnClose?.Invoke();
// return Task.FromResult(0); return Task.FromResult(0);
// } }
// public void SetProxy(string host, int port) public void SetProxy(string host, int port)
// { {
// throw new NotImplementedException(); throw new NotImplementedException();
// } }
// public void Dispose() public void Dispose()
// { {
// } }
// public void InvokeClose() public void InvokeClose()
// { {
// Connected = false; Connected = false;
// DisconnectTime = DateTime.UtcNow; DisconnectTime = DateTime.UtcNow;
// Reconnecting = true; Reconnecting = true;
// OnClose?.Invoke(); OnClose?.Invoke();
// } }
// public void InvokeOpen() public void InvokeOpen()
// { {
// OnOpen?.Invoke(); OnOpen?.Invoke();
// } }
// public void InvokeMessage(string data) public void InvokeMessage(string data)
// { {
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait(); OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
// } }
// public void SetProxy(ApiProxy proxy) public void SetProxy(ApiProxy proxy)
// { {
// throw new NotImplementedException(); throw new NotImplementedException();
// } }
// public void InvokeError(Exception error) public void InvokeError(Exception error)
// { {
// OnError?.Invoke(error); OnError?.Invoke(error);
// } }
// public Task ReconnectAsync() => Task.CompletedTask; public Task ReconnectAsync() => Task.CompletedTask;
// } }
//} }
@@ -13,37 +13,40 @@ using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
using CryptoExchange.Net.Testing.Implementations; using Newtonsoft.Json.Linq;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
internal class TestSocketClient: BaseSocketClient public class TestSocketClient: BaseSocketClient
{ {
public TestSubSocketClient SubClient { get; } public TestSubSocketClient SubClient { get; }
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
/// <summary> /// <summary>
/// Create a new instance of KucoinSocketClient /// Create a new instance of KucoinSocketClient
/// </summary> /// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param> /// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null) public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{ {
} }
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test") public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{ {
Initialize(options.Value); var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
optionsFunc(options);
Initialize(options);
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions)); SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object; SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com")); Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
} }
public TestSocket CreateSocket() public TestSocket CreateSocket()
{ {
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com")); Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/"); return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
} }
@@ -66,28 +69,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
Environment = new TestEnvironment("Live", "https://test.test") Environment = new TestEnvironment("Live", "https://test.test")
}; };
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions(); 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 public class TestSubSocketClient : SocketApiClient
{ {
private MessagePath _channelPath = MessagePath.Get().Property("channel"); private MessagePath _channelPath = MessagePath.Get().Property("channel");
private MessagePath _actionPath = MessagePath.Get().Property("action");
private MessagePath _topicPath = MessagePath.Get().Property("topic"); private MessagePath _topicPath = MessagePath.Get().Property("topic");
public Subscription TestSubscription { get; private set; } = null; public Subscription TestSubscription { get; private set; } = null;
@@ -98,7 +85,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
} }
/// <inheritdoc /> /// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
internal IWebsocket CreateSocketInternal(string address) internal IWebsocket CreateSocketInternal(string address)
{ {
@@ -123,7 +110,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var id = message.GetValue<string>(_channelPath); var id = message.GetValue<string>(_channelPath);
id ??= message.GetValue<string>(_topicPath); id ??= message.GetValue<string>(_topicPath);
return message.GetValue<string>(_actionPath) + "-" + id; return id;
} }
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct) public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
+1 -8
View File
@@ -11,9 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -37,10 +35,6 @@ Global
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -48,7 +42,6 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02} {AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7} SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
@@ -1,4 +1,4 @@
#if NETSTANDARD2_0 #if !NETSTANDARD2_1
namespace System.Diagnostics.CodeAnalysis namespace System.Diagnostics.CodeAnalysis
{ {
using System; using System;
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Security;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Converters.MessageParsing;
@@ -8,37 +9,71 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Api credentials, used to sign requests accessing private endpoints /// Api credentials, used to sign requests accessing private endpoints
/// </summary> /// </summary>
public class ApiCredentials public class ApiCredentials: IDisposable
{ {
/// <summary> /// <summary>
/// The api key / label to authenticate requests /// The api key to authenticate requests
/// </summary> /// </summary>
public string Key { get; set; } public SecureString? Key { get; }
/// <summary> /// <summary>
/// The api secret or private key to authenticate requests /// The api secret to authenticate requests
/// </summary> /// </summary>
public string Secret { get; set; } public SecureString? Secret { get; }
/// <summary> /// <summary>
/// Type of the credentials /// Type of the credentials
/// </summary> /// </summary>
public ApiCredentialsType CredentialType { get; set; } public ApiCredentialsType CredentialType { get; }
/// <summary> /// <summary>
/// Create Api credentials providing an api key and secret for authentication /// Create Api credentials providing an api key and secret for authentication
/// </summary> /// </summary>
/// <param name="key">The api key / label used for identification</param> /// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param> /// <param name="secret">The api secret used for signing</param>
/// <param name="credentialType">The type of credentials</param> public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = 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)
{ {
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty"); throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialType; CredentialType = credentialsType;
Key = key; Key = key.ToSecureString();
Secret = secret; Secret = secret.ToSecureString();
} }
/// <summary> /// <summary>
@@ -47,7 +82,8 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
public virtual ApiCredentials Copy() public virtual ApiCredentials Copy()
{ {
return new ApiCredentials(Key, Secret, CredentialType); // Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
} }
/// <summary> /// <summary>
@@ -56,7 +92,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="inputStream">The stream containing the json data</param> /// <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="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> /// <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 static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{ {
var accessor = new SystemTextJsonStreamMessageAccessor(); var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result) if (!accessor.Read(inputStream, false).Result)
@@ -66,9 +102,20 @@ namespace CryptoExchange.Net.Authentication
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret")); var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null) if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key.ToSecureString();
Secret = secret.ToSecureString();
inputStream.Seek(0, SeekOrigin.Begin); inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(key, secret); }
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Secret?.Dispose();
} }
} }
} }
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Base class for authentication providers /// Base class for authentication providers
/// </summary> /// </summary>
public abstract class AuthenticationProvider public abstract class AuthenticationProvider : IDisposable
{ {
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
@@ -28,22 +28,17 @@ namespace CryptoExchange.Net.Authentication
/// </summary> /// </summary>
protected byte[] _sBytes; protected byte[] _sBytes;
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key!;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="credentials"></param> /// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials) protected AuthenticationProvider(ApiCredentials credentials)
{ {
if (credentials.Key == null || credentials.Secret == null) if (credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed"); throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials; _credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret); _sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
} }
/// <summary> /// <summary>
@@ -63,9 +58,9 @@ namespace CryptoExchange.Net.Authentication
RestApiClient apiClient, RestApiClient apiClient,
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
ref IDictionary<string, object>? uriParameters, IDictionary<string, object> uriParameters,
ref IDictionary<string, object>? bodyParameters, IDictionary<string, object> bodyParameters,
ref Dictionary<string, string>? headers, Dictionary<string, string> headers,
bool auth, bool auth,
ArrayParametersSerialization arraySerialization, ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition, HttpMethodParameterPosition parameterPosition,
@@ -369,9 +364,9 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create(); var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem) if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{ {
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER #if NETSTANDARD2_1_OR_GREATER
// Read from pem private key // Read from pem private key
var key = _credentials.Secret! var key = _credentials.Secret!.GetString()
.Replace("\n", "") .Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "") .Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "") .Replace("-----END PRIVATE KEY-----", "")
@@ -386,7 +381,7 @@ namespace CryptoExchange.Net.Authentication
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml) else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
{ {
// Read from xml private key format // Read from xml private key format
rsa.FromXmlString(_credentials.Secret!); rsa.FromXmlString(_credentials.Secret!.GetString());
} }
else else
{ {
@@ -403,14 +398,10 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static string BytesToHexString(byte[] buff) protected static string BytesToHexString(byte[] buff)
{ {
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty; var result = string.Empty;
foreach (var t in buff) foreach (var t in buff)
result += t.ToString("X2"); result += t.ToString("X2");
return result; return result;
#endif
} }
/// <summary> /// <summary>
@@ -443,16 +434,6 @@ namespace CryptoExchange.Net.Authentication
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
} }
/// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary>
/// <param name="apiClient"></param>
/// <returns></returns>
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
{
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
}
/// <summary> /// <summary>
/// Return the serialized request body /// Return the serialized request body
/// </summary> /// </summary>
@@ -466,6 +447,12 @@ namespace CryptoExchange.Net.Authentication
else else
return serializer.Serialize(parameters); return serializer.Serialize(parameters);
} }
/// <inheritdoc />
public void Dispose()
{
_credentials?.Dispose();
}
} }
/// <inheritdoc /> /// <inheritdoc />
+1 -1
View File
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns> /// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge) public object? Get(string key, TimeSpan maxAge)
{ {
_cache.TryGetValue(key, out CacheItem? value); _cache.TryGetValue(key, out CacheItem value);
if (value == null) if (value == null)
return null; return null;
+8 -20
View File
@@ -1,9 +1,7 @@
using System; using System;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients
@@ -38,11 +36,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public bool OutputOriginalData { get; } public bool OutputOriginalData { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
/// <summary> /// <summary>
/// Api options /// Api options
/// </summary> /// </summary>
@@ -72,7 +65,10 @@ namespace CryptoExchange.Net.Clients
BaseAddress = baseAddress; BaseAddress = baseAddress;
if (apiCredentials != null) if (apiCredentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
}
} }
/// <summary> /// <summary>
@@ -83,25 +79,16 @@ namespace CryptoExchange.Net.Clients
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc /> /// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); public abstract string FormatSymbol(string baseAsset, string quoteAsset);
/// <inheritdoc /> /// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{ {
ApiOptions.ApiCredentials = credentials;
if (credentials != null) if (credentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
} }
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
} }
/// <summary> /// <summary>
@@ -110,6 +97,7 @@ namespace CryptoExchange.Net.Clients
public virtual void Dispose() public virtual void Dispose()
{ {
_disposing = true; _disposing = true;
AuthenticationProvider?.Dispose();
} }
} }
} }
+1 -36
View File
@@ -12,28 +12,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public abstract class BaseClient : IDisposable 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> /// <summary>
/// The name of the API the client is for /// The name of the API the client is for
/// </summary> /// </summary>
@@ -49,9 +27,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected internal ILogger _logger; protected internal ILogger _logger;
private object _versionLock = new object();
private Version _exchangeVersion;
/// <summary> /// <summary>
/// Provided client options /// Provided client options
/// </summary> /// </summary>
@@ -82,7 +57,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
ClientOptions = options; ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}"); _logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}");
} }
/// <summary> /// <summary>
@@ -109,16 +84,6 @@ namespace CryptoExchange.Net.Clients
return apiClient; 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> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+54 -79
View File
@@ -18,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests; using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients
{ {
@@ -154,7 +155,6 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>( protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -162,8 +162,7 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? parameters, ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null, int? weight = null) where T : class
int? weightSingleLimiter = null)
{ {
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>( return SendAsync<T>(
@@ -173,8 +172,7 @@ namespace CryptoExchange.Net.Clients
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null, parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken, cancellationToken,
additionalHeaders, additionalHeaders,
weight, weight);
weightSingleLimiter);
} }
/// <summary> /// <summary>
@@ -188,7 +186,6 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>( protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
@@ -197,23 +194,21 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null, int? weight = null) where T : class
int? weightSingleLimiter = null)
{ {
string? cacheKey = null; var key = baseAddress + definition + uriParameters?.ToFormData();
if (ShouldCache(definition)) if (ShouldCache(definition))
{ {
cacheKey = baseAddress + definition + uriParameters?.ToFormData(); _logger.CheckingCache(key);
_logger.CheckingCache(cacheKey); var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null) if (cachedValue != null)
{ {
_logger.CacheHit(cacheKey); _logger.CacheHit(key);
var original = (WebCallResult<T>)cachedValue; var original = (WebCallResult<T>)cachedValue;
return original.Cached(); return original.Cached();
} }
_logger.CacheNotHit(cacheKey); _logger.CacheNotHit(key);
} }
int currentTry = 0; int currentTry = 0;
@@ -222,7 +217,7 @@ namespace CryptoExchange.Net.Clients
currentTry++; currentTry++;
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter).ConfigureAwait(false); var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
if (!prepareResult) if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!); return new WebCallResult<T>(prepareResult.Error!);
@@ -233,7 +228,7 @@ namespace CryptoExchange.Net.Clients
uriParameters, uriParameters,
bodyParameters, bodyParameters,
additionalHeaders); 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)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++; TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (!result) if (!result)
@@ -247,7 +242,7 @@ namespace CryptoExchange.Net.Clients
if (result.Success && if (result.Success &&
ShouldCache(definition)) ShouldCache(definition))
{ {
_cache.Add(cacheKey!, result); _cache.Add(key, result);
} }
return result; return result;
@@ -263,7 +258,6 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param> /// <param name="weight">Override the request weight for this request</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync( protected virtual async Task<CallResult> PrepareAsync(
@@ -272,9 +266,10 @@ namespace CryptoExchange.Net.Clients
RequestDefinition definition, RequestDefinition definition,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null, int? weight = null)
int? weightSingleLimiter = null)
{ {
var requestWeight = weight ?? definition.Weight;
// Time sync // Time sync
if (definition.Authenticated) if (definition.Authenticated)
{ {
@@ -300,7 +295,6 @@ namespace CryptoExchange.Net.Clients
} }
// Rate limiting // Rate limiting
var requestWeight = weight ?? definition.Weight;
if (requestWeight != 0) if (requestWeight != 0)
{ {
if (definition.RateLimitGate == null) if (definition.RateLimitGate == null)
@@ -322,8 +316,7 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var singleRequestWeight = weightSingleLimiter ?? 1; var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult(limitResult.Error!); return new CallResult(limitResult.Error!);
} }
@@ -350,15 +343,15 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders) Dictionary<string, string>? additionalHeaders)
{ {
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters); var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters); var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters);
var uri = new Uri(baseAddress.AppendPath(definition.Path)); var uri = new Uri(baseAddress.AppendPath(definition.Path));
var arraySerialization = definition.ArraySerialization ?? ArraySerialization; var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat; var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method]; var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
Dictionary<string, string>? headers = null; var headers = new Dictionary<string, string>();
if (AuthenticationProvider != null) if (AuthenticationProvider != null)
{ {
try try
@@ -367,9 +360,9 @@ namespace CryptoExchange.Net.Clients
this, this,
uri, uri,
definition.Method, definition.Method,
ref uriParams, uriParams,
ref bodyParams, bodyParams,
ref headers, headers,
definition.Authenticated, definition.Authenticated,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
@@ -382,18 +375,14 @@ namespace CryptoExchange.Net.Clients
} }
} }
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
if (uriParams != null) uri = uri.SetParameters(uriParams, arraySerialization);
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId); var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
if (headers != null) foreach (var header in headers)
{ request.AddHeader(header.Key, header.Value);
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null) if (additionalHeaders != null)
{ {
@@ -414,7 +403,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody) if (parameterPosition == HttpMethodParameterPosition.InBody)
{ {
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParams != null && bodyParams.Count != 0) if (bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType); WriteParamBody(request, bodyParams, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, contentType);
@@ -624,7 +613,7 @@ namespace CryptoExchange.Net.Clients
paramString = $" with request body '{request.Content}'"; paramString = $" with request body '{request.Content}'";
var headers = request.GetHeaders(); var headers = request.GetHeaders();
if (headers.Count != 0) if (headers.Any())
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")); paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++; TotalRequestsMade++;
@@ -700,21 +689,10 @@ namespace CryptoExchange.Net.Clients
} }
// Json response received // Json response received
var parsedError = TryParseError(response.ResponseHeaders, accessor); var parsedError = TryParseError(accessor);
if (parsedError != null) if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
{
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
}
// Success status code, but TryParseError determined it was an error response // Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
@@ -748,13 +726,12 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// This method will be called for each response to be able to check if the response is an error or not. /// 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.
/// If the response is an error this method should return the parsed error, else it should return null /// If the response is an error this method should return the parsed error, else it should return null
/// </summary> /// </summary>
/// <param name="accessor">Data accessor</param> /// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns> /// <returns>Null if not an error, Error otherwise</returns>
protected virtual Error? TryParseError(IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor) => null; protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null;
/// <summary> /// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@@ -771,7 +748,7 @@ namespace CryptoExchange.Net.Clients
// Only retry once // Only retry once
return false; return false;
if (callResult.Error is ServerRateLimitError if ((int?)callResult.ResponseStatusCode == 429
&& ClientOptions.RateLimiterEnabled && ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail && ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null) && gate != null)
@@ -826,12 +803,12 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InUri) if (parameterPosition == HttpMethodParameterPosition.InUri)
{ {
foreach (var parameter in parameters) foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()!); uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
} }
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null; var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null; var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
if (AuthenticationProvider != null) if (AuthenticationProvider != null)
{ {
try try
@@ -840,9 +817,9 @@ namespace CryptoExchange.Net.Clients
this, this,
uri, uri,
method, method,
ref uriParameters, uriParameters,
ref bodyParameters, bodyParameters,
ref headers, headers,
signed, signed,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
@@ -855,18 +832,24 @@ namespace CryptoExchange.Net.Clients
} }
} }
// Sanity check
foreach (var param in parameters)
{
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
{
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
$"should return provided parameters in either the uri or body parameters output");
}
}
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
if (uriParameters != null) uri = uri.SetParameters(uriParameters, arraySerialization);
uri = uri.SetParameters(uriParameters, arraySerialization);
var request = RequestFactory.Create(method, uri, requestId); var request = RequestFactory.Create(method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
if (headers != null) foreach (var header in headers)
{ request.AddHeader(header.Key, header.Value);
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null) if (additionalHeaders != null)
{ {
@@ -887,7 +870,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody) if (parameterPosition == HttpMethodParameterPosition.InBody)
{ {
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters?.Any() == true) if (bodyParameters.Any())
WriteParamBody(request, bodyParameters, contentType); WriteParamBody(request, bodyParameters, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, contentType);
@@ -908,8 +891,8 @@ namespace CryptoExchange.Net.Clients
{ {
// Write the parameters as json in the body // Write the parameters as json in the body
string stringData; string stringData;
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
stringData = CreateSerializer().Serialize(value); stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
else else
stringData = CreateSerializer().Serialize(parameters); stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType); request.SetContent(stringData, contentType);
@@ -980,14 +963,6 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns> /// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
+14 -68
View File
@@ -8,6 +8,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -72,11 +73,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>(); 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 /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
{ {
@@ -158,7 +154,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -193,10 +189,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe")); return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null) if (subscription.Authenticated && AuthenticationProvider == null)
{
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError()); return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
}
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
@@ -216,7 +209,7 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
// Get a new or existing socket connection // Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<UpdateSubscription>(null); return socketResult.As<UpdateSubscription>(null);
@@ -258,7 +251,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused")); return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
} }
var waitEvent = new AsyncResetEvent(false); var waitEvent = new ManualResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection); var subQuery = subscription.GetSubQuery(socketConnection);
if (subQuery != null) if (subQuery != null)
{ {
@@ -276,7 +269,7 @@ namespace CryptoExchange.Net.Clients
{ {
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString()); _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 // If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
await socketConnection.CloseAsync(subscription).ConfigureAwait(false); await socketConnection.CloseAsync(subscription, isTimeout).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!); return new CallResult<UpdateSubscription>(subResult.Error!);
} }
} }
@@ -408,7 +401,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError()); return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId); _logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false); var authRequest = GetAuthenticationRequest(socket);
if (authRequest != null) if (authRequest != null)
{ {
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@@ -422,10 +415,9 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message; result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!; return new CallResult(result.Error)!;
} }
_logger.Authenticated(socket.SocketId);
} }
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true; socket.Authenticated = true;
return new CallResult(null); return new CallResult(null);
} }
@@ -434,7 +426,7 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection /// Should return the request which can be used to authenticate a socket connection
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException(); protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
/// <summary> /// <summary>
/// Adds a system subscription. Used for example to reply to ping requests /// Adds a system subscription. Used for example to reply to ping requests
@@ -484,28 +476,23 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param> /// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</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="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> /// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null) protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
{ {
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected) 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.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType() && s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated) && (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected); && s.Value.Connected);
SocketConnection connection; SocketConnection connection;
if (!dedicatedRequestConnection) if (!dedicatedRequestConnection)
{ {
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value; connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
} }
else else
{ {
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value; connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).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 != null)
@@ -529,15 +516,7 @@ namespace CryptoExchange.Net.Clients
var socket = CreateSocket(connectionAddress.Data!); var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address); var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage; socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync; socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
if (dedicatedRequestConnection)
{
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
{
IsDedicatedRequestConnection = dedicatedRequestConnection,
Authenticated = authenticated
};
}
foreach (var ptg in PeriodicTaskRegistrations) foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback); socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
@@ -556,19 +535,6 @@ 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> /// <summary>
/// Connect a socket /// Connect a socket
/// </summary> /// </summary>
@@ -670,7 +636,7 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
var socketList = socketConnections.Values; var socketList = socketConnections.Values;
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)) foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
tasks.Add(connection.CloseAsync()); tasks.Add(connection.CloseAsync());
} }
@@ -711,25 +677,6 @@ namespace CryptoExchange.Net.Clients
return new CallResult(null); return new CallResult(null);
} }
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| !socketConnections.Any())
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@@ -839,10 +786,9 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Preprocess a stream message /// Preprocess a stream message
/// </summary> /// </summary>
/// <param name="connection"></param>
/// <param name="type"></param> /// <param name="type"></param>
/// <param name="data"></param> /// <param name="data"></param>
/// <returns></returns> /// <returns></returns>
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data; public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
} }
} }
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
var result = Activator.CreateInstance(objectType); var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader); var arr = JArray.Load(reader);
return ParseObject(arr, result!, objectType); return ParseObject(arr, result, objectType);
} }
private static object ParseObject(JArray arr, object result, Type objectType) private static object ParseObject(JArray arr, object result, Type objectType)
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
var count = 0; var count = 0;
if (innerArray.Count == 0) if (innerArray.Count == 0)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!; var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else if (innerArray[0].Type == JTokenType.Array) else if (innerArray[0].Type == JTokenType.Array)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!; var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
foreach (var obj in innerArray) foreach (var obj in innerArray)
{ {
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!); arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
count++; count++;
} }
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else else
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!; var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!); arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
continue; continue;
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
object? value; object? value;
if (converterAttribute != null) if (converterAttribute != null)
{ {
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}}); value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
} }
else if (conversionAttribute != null) else if (conversionAttribute != null)
{ {
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
else if ((property.PropertyType == typeof(decimal) else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?)) || property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0)) && (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
var v = value.ToString(); var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec)) if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
last = arrayProp.Index; last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop); var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null) if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!)); writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
else if (!IsSimple(prop.PropertyType)) else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value)); serializer.Serialize(writer, prop.GetValue(value));
else else
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute => private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!); (T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
private static T? GetCustomAttribute<T>(Type type) where T : Attribute => private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!); (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
} }
} }
@@ -1,62 +0,0 @@
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> /// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var value = reader.Value?.ToString()!.ToLower().Trim(); var value = reader.Value?.ToString().ToLower().Trim();
if (value == null || value == "") if (value == null || value == "")
{ {
if (Nullable.GetUnderlyingType(objectType) != null) if (Nullable.GetUnderlyingType(objectType) != null)
@@ -1,4 +1,5 @@
using Newtonsoft.Json; using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; 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 // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader); using var jsonTextReader = new JsonTextReader(reader);
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType)!; return (JsonConverter)Activator.CreateInstance(converterType);
} }
private class ArrayPropertyInfo private class ArrayPropertyInfo
@@ -32,73 +32,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!; public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public Type? JsonConverterType { get; set; } public Type? JsonConverterType { get; set; }
public bool DefaultDeserialization { get; set; } public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
} }
private class ArrayConverterInner<T> : JsonConverter<T> private class ArrayConverterInner<T> : JsonConverter<T>
{ {
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>(); 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) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) // TODO
{ throw new NotImplementedException();
writer.WriteNullValue();
return;
}
writer.WriteStartArray();
var valueType = value.GetType();
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
typeAttributes = CacheTypeAttributes(valueType);
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
{
writer.WriteNullValue();
last += 1;
}
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null)
{
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter);
}
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.PropertyInfo.PropertyType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
writer.WriteEndArray();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -107,21 +51,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
return default; return default;
var result = Activator.CreateInstance(typeToConvert)!; var result = Activator.CreateInstance(typeToConvert);
return (T)ParseObject(ref reader, result, typeToConvert, options); return (T)ParseObject(ref reader, result, typeToConvert);
}
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) private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
@@ -139,8 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
ArrayProperty = att, ArrayProperty = att,
PropertyInfo = property, PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null, DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType, JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
}); });
} }
@@ -148,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return attributes; return attributes;
} }
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options) private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array"); throw new Exception("Not an array");
@@ -162,58 +92,40 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray) if (reader.TokenType == JsonTokenType.EndArray)
break; break;
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index); var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any()) if (attribute == null)
{
index++;
continue; continue;
}
foreach (var attribute in indexAttributes) var targetType = attribute.PropertyInfo.PropertyType;
object? value = null;
if (attribute.JsonConverterType != null)
{ {
var targetType = attribute.TargetType; // Has JsonConverter attribute
object? value = null; var options = new JsonSerializerOptions();
if (attribute.JsonConverterType != null) 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
{ {
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) JsonTokenType.Null => null,
{ JsonTokenType.False => false,
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!; JsonTokenType.True => true,
newOptions = new JsonSerializerOptions JsonTokenType.String => reader.GetString(),
{ JsonTokenType.Number => reader.GetDecimal(),
NumberHandling = SerializerOptions.WithConverters.NumberHandling, _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive, };
Converters = { converter },
};
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
}
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, newOptions);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
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++; index++;
} }
@@ -1,46 +0,0 @@
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) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType)!; return (JsonConverter)Activator.CreateInstance(converterType);
} }
private class BoolConverterInner<T> : JsonConverter<T> private class BoolConverterInner<T> : JsonConverter<T>
@@ -1,27 +0,0 @@
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,4 +1,5 @@
using System; using Microsoft.Extensions.Primitives;
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
@@ -27,7 +28,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType)!; return (JsonConverter)Activator.CreateInstance(converterType);
} }
private class DateTimeConverterInner<T> : JsonConverter<T> private class DateTimeConverterInner<T> : JsonConverter<T>
@@ -57,7 +58,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.GetString(); var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue) if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1" || stringValue == "-1"
|| stringValue == "0001-01-01T00:00:00Z"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0) || double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{ {
return default; return default;
@@ -74,9 +74,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
{
writer.WriteNullValue(); writer.WriteNullValue();
}
else else
{ {
var dtValue = (DateTime)(object)value; var dtValue = (DateTime)(object)value;
@@ -19,33 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String) if (reader.TokenType == JsonTokenType.String)
{ {
var value = reader.GetString(); var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrEmpty(value))
return null; return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal)) return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
// 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;
}
} }
try return reader.GetDecimal();
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -1,37 +0,0 @@
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);
}
}
@@ -211,37 +211,5 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
} }
/// <summary>
/// Get the enum value from a string
/// </summary>
/// <typeparam name="T">Enum type</typeparam>
/// <param name="value">String value</param>
/// <returns></returns>
public static T? ParseString<T>(string value) where T : Enum
{
var type = typeof(T);
if (!_mapping.TryGetValue(type, out var enumMapping))
enumMapping = AddMapping(type);
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if (mapping.Equals(default(KeyValuePair<object, string>)))
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (!mapping.Equals(default(KeyValuePair<object, string>)))
{
return (T)mapping.Key;
}
try
{
// If no explicit mapping is found try to parse string
return (T)Enum.Parse(type, value, true);
}
catch (Exception)
{
return default;
}
}
} }
} }
@@ -1,31 +0,0 @@
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)!;
}
}
}
@@ -1,42 +0,0 @@
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);
}
}
}
@@ -1,6 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
using System.Globalization;
namespace CryptoExchange.Net.Converters.SystemTextJson namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
@@ -1,41 +0,0 @@
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,7 +21,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
protected JsonDocument? _document; protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc /> /// <inheritdoc />
public bool IsJson { get; set; } public bool IsJson { get; set; }
@@ -32,21 +31,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public object? Underlying => throw new NotImplementedException(); public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null) public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{ {
@@ -58,7 +42,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions); var result = _document.Deserialize(type, _serializerOptions);
return new CallResult<object>(result!); return new CallResult<object>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -66,11 +50,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; 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]")); 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 /> /// <inheritdoc />
@@ -81,7 +60,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions); var result = _document.Deserialize<T>(_serializerOptions);
return new CallResult<T>(result!); return new CallResult<T>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -142,39 +121,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default; return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array) if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
}
catch { }
return default; return default;
}
if (typeof(T) == typeof(string))
{
if (value.Value.ValueKind == JsonValueKind.Number)
return (T)(object)value.Value.GetInt64().ToString();
}
return value.Value.Deserialize<T>(); return value.Value.Deserialize<T>();
} }
/// <inheritdoc /> /// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path) public List<T?>? GetValues<T>(MessagePath path) => throw new NotImplementedException();
{
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) private JsonElement? GetPathNode(MessagePath path)
{ {
@@ -239,20 +192,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true; public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(): base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream) public async Task<CallResult> Read(Stream stream, bool bufferStream)
{ {
@@ -303,7 +242,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
_stream?.Dispose(); _stream?.Dispose();
_stream = null; _stream = null;
_document?.Dispose();
_document = null; _document = null;
} }
@@ -316,20 +254,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private ReadOnlyMemory<byte> _bytes; private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor() : base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data) public CallResult Read(ReadOnlyMemory<byte> data)
{ {
@@ -337,14 +261,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try 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); _document = JsonDocument.Parse(data);
IsJson = true; IsJson = true;
return new CallResult(null); return new CallResult(null);
@@ -373,7 +289,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Clear() public override void Clear()
{ {
_bytes = null; _bytes = null;
_document?.Dispose();
_document = null; _document = null;
} }
} }
+12 -12
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>8.7.0</PackageVersion> <PackageVersion>7.8.0</PackageVersion>
<AssemblyVersion>8.7.0</AssemblyVersion> <AssemblyVersion>7.8.0</AssemblyVersion>
<FileVersion>8.7.0</FileVersion> <FileVersion>7.8.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -20,7 +20,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion> <LangVersion>10.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -35,7 +35,7 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'"> <ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0"> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@@ -48,17 +48,17 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0"> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="9.0.0" /> <PackageReference Include="System.Text.Json" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+22 -105
View File
@@ -1,11 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -16,26 +11,14 @@ namespace CryptoExchange.Net
{ {
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; 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> /// <summary>
/// The last used id, use NextId() to get the next id and up this /// The last used id, use NextId() to get the next id and up this
/// </summary> /// </summary>
private static int _lastId; private static int _lastId;
/// <summary>
/// Lock for id generating
/// </summary>
private static object _idLock = new();
/// <summary> /// <summary>
/// Clamp a value between a min and max /// Clamp a value between a min and max
@@ -75,11 +58,6 @@ namespace CryptoExchange.Net
{ {
value -= offset; value -= offset;
} }
else if(roundingType == RoundingType.Up)
{
if (offset != 0)
value += (step.Value - offset);
}
else else
{ {
if (offset < step / 2) if (offset < step / 2)
@@ -132,23 +110,17 @@ namespace CryptoExchange.Net
} }
/// <summary> /// <summary>
/// Rounds a value down /// Rounds a value down to
/// </summary> /// </summary>
/// <param name="i"></param>
/// <param name="decimalPlaces"></param>
/// <returns></returns>
public static decimal RoundDown(decimal i, double decimalPlaces) public static decimal RoundDown(decimal i, double decimalPlaces)
{ {
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
return Math.Floor(i * power) / power; 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> /// <summary>
/// Strips any trailing zero's of a decimal value, useful when converting the value to string. /// Strips any trailing zero's of a decimal value, useful when converting the value to string.
/// </summary> /// </summary>
@@ -163,13 +135,24 @@ namespace CryptoExchange.Net
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique /// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static int NextId() => Interlocked.Increment(ref _lastId); public static int NextId()
{
lock (_idLock)
{
_lastId += 1;
return _lastId;
}
}
/// <summary> /// <summary>
/// Return the last unique id that was generated /// Return the last unique id that was generated
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static int LastId() => _lastId; public static int LastId()
{
lock (_idLock)
return _lastId;
}
/// <summary> /// <summary>
/// Generate a random string of specified length /// Generate a random string of specified length
@@ -180,7 +163,7 @@ namespace CryptoExchange.Net
{ {
var randomChars = new char[length]; var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER #if NETSTANDARD2_1_OR_GREATER
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else #else
@@ -208,71 +191,5 @@ namespace CryptoExchange.Net
return source + RandomString(totalLength - source.Length); 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;
}
} }
} }
+111 -145
View File
@@ -4,12 +4,11 @@ using System.IO.Compression;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security;
using System.Text; using System.Text;
using System.Web; using System.Web;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System.Globalization; using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -67,7 +66,7 @@ namespace CryptoExchange.Net
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&"; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
} }
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
@@ -111,7 +110,93 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
} }
} }
return formData.ToString()!; return formData.ToString();
}
/// <summary>
/// Get the string the secure string is representing
/// </summary>
/// <param name="source">The source secure string</param>
/// <returns></returns>
public static string GetString(this SecureString source)
{
lock (source)
{
string result;
var length = source.Length;
var pointer = IntPtr.Zero;
var chars = new char[length];
try
{
pointer = Marshal.SecureStringToBSTR(source);
Marshal.Copy(pointer, chars, 0, length);
result = string.Join("", chars);
}
finally
{
if (pointer != IntPtr.Zero)
{
Marshal.ZeroFreeBSTR(pointer);
}
}
return result;
}
}
/// <summary>
/// Are 2 secure strings equal
/// </summary>
/// <param name="ss1">Source secure string</param>
/// <param name="ss2">Compare secure string</param>
/// <returns>True if equal by value</returns>
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
{
IntPtr bstr1 = IntPtr.Zero;
IntPtr bstr2 = IntPtr.Zero;
try
{
bstr1 = Marshal.SecureStringToBSTR(ss1);
bstr2 = Marshal.SecureStringToBSTR(ss2);
int length1 = Marshal.ReadInt32(bstr1, -4);
int length2 = Marshal.ReadInt32(bstr2, -4);
if (length1 == length2)
{
for (int x = 0; x < length1; ++x)
{
byte b1 = Marshal.ReadByte(bstr1, x);
byte b2 = Marshal.ReadByte(bstr2, x);
if (b1 != b2) return false;
}
}
else
{
return false;
}
return true;
}
finally
{
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
}
}
/// <summary>
/// Create a secure string from a string
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static SecureString ToSecureString(this string source)
{
var secureString = new SecureString();
foreach (var c in source)
secureString.AppendChar(c);
secureString.MakeReadOnly();
return secureString;
} }
/// <summary> /// <summary>
@@ -189,16 +274,6 @@ namespace CryptoExchange.Net
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName); 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> /// <summary>
/// Format an exception and inner exception to a readable string /// Format an exception and inner exception to a readable string
/// </summary> /// </summary>
@@ -243,6 +318,26 @@ namespace CryptoExchange.Net
return url.TrimEnd('/'); return url.TrimEnd('/');
} }
/// <summary>
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
/// </summary>
/// <param name="path">The total path string</param>
/// <param name="values">The values to fill</param>
/// <returns></returns>
public static string FillPathParameters(this string path, params string[] values)
{
foreach (var value in values)
{
var index = path.IndexOf("{}", StringComparison.Ordinal);
if (index >= 0)
{
path = path.Remove(index, 2);
path = path.Insert(index, value);
}
}
return path;
}
/// <summary> /// <summary>
/// Create a new uri with the provided parameters as query /// Create a new uri with the provided parameters as query
/// </summary> /// </summary>
@@ -358,7 +453,7 @@ namespace CryptoExchange.Net
} }
/// <summary> /// <summary>
/// Decompress using GzipStream /// Decompress using Gzip
/// </summary> /// </summary>
/// <param name="data"></param> /// <param name="data"></param>
/// <returns></returns> /// <returns></returns>
@@ -366,141 +461,12 @@ namespace CryptoExchange.Net
{ {
using var decompressedStream = new MemoryStream(); using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream); deflateStream.CopyTo(decompressedStream);
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); 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,87 +8,131 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients namespace CryptoExchange.Net.Interfaces.CommonClients
{ {
/// <summary> /// <summary>
/// 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. /// Common rest client endpoints
/// </summary> /// </summary>
public interface IBaseRestClient public interface IBaseRestClient
{ {
/// <summary> /// <summary>
/// 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. /// The name of the exchange
/// </summary> /// </summary>
string ExchangeName { get; } string ExchangeName { get; }
/// <summary> /// <summary>
/// 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. /// Should be triggered on order placing
/// </summary> /// </summary>
event Action<OrderId> OnOrderPlaced; event Action<OrderId> OnOrderPlaced;
/// <summary> /// <summary>
/// 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. /// Should be triggered on order cancelling
/// </summary> /// </summary>
event Action<OrderId> OnOrderCanceled; event Action<OrderId> OnOrderCanceled;
/// <summary> /// <summary>
/// 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. /// Get the symbol name based on a base and quote asset
/// </summary> /// </summary>
/// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote asset</param>
/// <returns></returns>
string GetSymbolName(string baseAsset, string quoteAsset); string GetSymbolName(string baseAsset, string quoteAsset);
/// <summary> /// <summary>
/// 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. /// Get a list of symbols for the exchange
/// </summary> /// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default); Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get a ticker for the exchange
/// </summary> /// </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); Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get a list of tickers for the exchange
/// </summary> /// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default); Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get a list of candles for a given symbol on the exchange
/// </summary> /// </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); Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get the order book for a symbol
/// </summary> /// </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); Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// The recent trades for a symbol
/// </summary> /// </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); Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get balances
/// </summary> /// </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); Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get an order by id
/// </summary> /// </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); Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get trades for an order by id
/// </summary> /// </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); Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get a list of open orders
/// </summary> /// </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); Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Get a list of closed orders
/// </summary> /// </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); Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
/// <summary> /// <summary>
/// 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. /// Cancel an order by id
/// </summary> /// </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); Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
} }
} }
@@ -7,18 +7,30 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Interfaces.CommonClients namespace CryptoExchange.Net.Interfaces.CommonClients
{ {
/// <summary> /// <summary>
/// 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. /// Common futures endpoints
/// </summary> /// </summary>
public interface IFuturesClient : IBaseRestClient public interface IFuturesClient : IBaseRestClient
{ {
/// <summary> /// <summary>
/// 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. /// Place an order
/// </summary> /// </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); 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> /// <summary>
/// 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. /// Get position
/// </summary> /// </summary>
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
/// <returns></returns>
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default); Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
} }
} }
@@ -6,13 +6,22 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces.CommonClients namespace CryptoExchange.Net.Interfaces.CommonClients
{ {
/// <summary> /// <summary>
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="" /> for more info. /// Common spot endpoints
/// </summary> /// </summary>
public interface ISpotClient: IBaseRestClient public interface ISpotClient: IBaseRestClient
{ {
/// <summary> /// <summary>
/// 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. /// Place an order
/// </summary> /// </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); 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,8 +1,4 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
{ {
@@ -21,10 +17,8 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <param name="baseAsset">The base asset</param> /// <param name="baseAsset">The base asset</param>
/// <param name="quoteAsset">The quote 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> /// <returns></returns>
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); string FormatSymbol(string baseAsset, string quoteAsset);
/// <summary> /// <summary>
/// Set the API credentials for this API client /// Set the API credentials for this API client
@@ -32,12 +26,5 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="credentials"></param> /// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials; void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api crentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
} }
} }
@@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
{ {
@@ -26,7 +25,7 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="connection"></param> /// <param name="connection"></param>
/// <param name="message"></param> /// <param name="message"></param>
/// <returns></returns> /// <returns></returns>
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message); CallResult Handle(SocketConnection connection, DataEvent<object> message);
/// <summary> /// <summary>
/// Get the type the message should be deserialized to /// Get the type the message should be deserialized to
/// </summary> /// </summary>
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System; using System;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
@@ -24,12 +23,5 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="options">Options for the order book</param> /// <param name="options">Options for the order book</param>
/// <returns></returns> /// <returns></returns>
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null); 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,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Net.Http; using System.Net.Http;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -23,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestWeight">The weight of the request</param> /// <param name="requestWeight">The weight of the request</param>
/// <param name="ct">Cancellation token to cancel waiting</param> /// <param name="ct">Cancellation token to cancel waiting</param>
/// <returns>The time in milliseconds spend waiting</returns> /// <returns>The time in milliseconds spend waiting</returns>
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, string? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct); Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
} }
} }
@@ -24,13 +24,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param> /// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param> /// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null); void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
} }
} }
@@ -62,7 +62,7 @@ namespace CryptoExchange.Net.Interfaces
Task UnsubscribeAsync(UpdateSubscription subscription); Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary> /// <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. /// Prepare connections which can subsequently be used for sending websocket requests.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task<CallResult> PrepareConnectionsAsync(); Task<CallResult> PrepareConnectionsAsync();
+2 -11
View File
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Websocket message received event /// Websocket message received event
/// </summary> /// </summary>
event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage; event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
/// <summary> /// <summary>
/// Websocket sent event, RequestId as parameter /// Websocket sent event, RequestId as parameter
/// </summary> /// </summary>
@@ -27,10 +27,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
event Func<int, Task>? OnRequestRateLimited; event Func<int, Task>? OnRequestRateLimited;
/// <summary> /// <summary>
/// Connection was ratelimited and couldn't be established
/// </summary>
event Func<Task>? OnConnectRateLimited;
/// <summary>
/// Websocket error event /// Websocket error event
/// </summary> /// </summary>
event Func<Exception, Task> OnError; event Func<Exception, Task> OnError;
@@ -82,7 +78,7 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="data"></param> /// <param name="data"></param>
/// <param name="weight"></param> /// <param name="weight"></param>
bool Send(int id, string data, int weight); void Send(int id, string data, int weight);
/// <summary> /// <summary>
/// Reconnect the socket /// Reconnect the socket
/// </summary> /// </summary>
@@ -93,10 +89,5 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task CloseAsync(); Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
} }
} }
-47
View File
@@ -1,47 +0,0 @@
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 seperator
/// </summary>
public const string ClientOrderIdSeperator = "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="allowValueAdjustement"></param>
/// <returns></returns>
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement)
{
var reservedLength = brokerId.Length + ClientOrderIdSeperator.Length;
if ((clientOrderId?.Length + reservedLength) > maxLength)
return clientOrderId!;
if (!string.IsNullOrEmpty(clientOrderId))
{
if (allowValueAdjustement)
clientOrderId = brokerId + ClientOrderIdSeperator + clientOrderId;
return clientOrderId!;
}
else
{
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeperator, maxLength);
}
return clientOrderId;
}
}
}
@@ -3,8 +3,7 @@ using System;
namespace CryptoExchange.Net.Logging.Extensions namespace CryptoExchange.Net.Logging.Extensions
{ {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member internal static class CryptoExchangeWebSocketClientLoggingExtension
public static class CryptoExchangeWebSocketClientLoggingExtension
{ {
private static readonly Action<ILogger, int, Exception?> _connecting; private static readonly Action<ILogger, int, Exception?> _connecting;
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed; private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
@@ -25,7 +24,6 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException; private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished; private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage; private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseConfirmation;
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage; private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage; private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage; private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
@@ -34,8 +32,6 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished; private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck; private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect; private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
static CryptoExchangeWebSocketClientLoggingExtension() static CryptoExchangeWebSocketClientLoggingExtension()
{ {
@@ -155,7 +151,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes"); "[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
_receiveLoopStoppedWithException = LoggerMessage.Define<int>( _receiveLoopStoppedWithException = LoggerMessage.Define<int>(
LogLevel.Error, LogLevel.Warning,
new EventId(1024, "ReceiveLoopStoppedWithException"), new EventId(1024, "ReceiveLoopStoppedWithException"),
"[Sckt {SocketId}] receive loop stopped with exception"); "[Sckt {SocketId}] receive loop stopped with exception");
@@ -170,25 +166,9 @@ namespace CryptoExchange.Net.Logging.Extensions
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}"); "[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>( _noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Warning, LogLevel.Debug,
new EventId(1027, "NoDataReceiveTimeoutReconnect"), new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket"); "[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
_receivedCloseConfirmation = LoggerMessage.Define<int, string, string>(
LogLevel.Debug,
new EventId(1028, "ReceivedCloseMessage"),
"[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(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( public static void SocketConnecting(
@@ -305,12 +285,6 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null); _receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
} }
public static void SocketReceivedCloseConfirmation(
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
{
_receivedCloseConfirmation(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
}
public static void SocketReceivedPartialMessage( public static void SocketReceivedPartialMessage(
this ILogger logger, int socketId, int countBytes) this ILogger logger, int socketId, int countBytes)
{ {
@@ -358,17 +332,5 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null); _noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
} }
public static void SocketProcessingStateChanged(
this ILogger logger, int socketId, string prevState, string newState)
{
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
}
public static void SocketPingTimeout(
this ILogger logger, int socketId)
{
_socketPingTimeout(logger, socketId, null);
}
} }
} }
@@ -3,8 +3,7 @@ using System;
namespace CryptoExchange.Net.Logging.Extensions namespace CryptoExchange.Net.Logging.Extensions
{ {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member internal static class RateLimitGateLoggingExtensions
public static class RateLimitGateLoggingExtensions
{ {
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed; private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed; private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
@@ -6,8 +6,7 @@ using System.Net.Http;
namespace CryptoExchange.Net.Logging.Extensions namespace CryptoExchange.Net.Logging.Extensions
{ {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member internal static class RestApiClientLoggingExtensions
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?> _restApiErrorReceived;
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived; private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
@@ -3,8 +3,7 @@ using System;
namespace CryptoExchange.Net.Logging.Extensions namespace CryptoExchange.Net.Logging.Extensions
{ {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member internal static class SocketApiClientLoggingExtension
public static class SocketApiClientLoggingExtension
{ {
private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection; private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection;
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment; private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment;
@@ -22,7 +21,6 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, Exception?> _disposingSocketClient; private static readonly Action<ILogger, Exception?> _disposingSocketClient;
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription; private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections; private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
static SocketApiClientLoggingExtension() static SocketApiClientLoggingExtension()
{ {
@@ -105,11 +103,6 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Information, LogLevel.Information,
new EventId(3017, "ReconnectingAll"), new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections"); "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) public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -191,10 +184,5 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_reconnectingAllConnections(logger, connectionCount, null); _reconnectingAllConnections(logger, connectionCount, null);
} }
public static void AddingRetryAfterGuard(this ILogger logger, DateTime retryAfter)
{
_addingRetryAfterGuard(logger, retryAfter, null);
}
} }
} }
@@ -4,8 +4,7 @@ using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions namespace CryptoExchange.Net.Logging.Extensions
{ {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member internal static class SocketConnectionLoggingExtension
public static class SocketConnectionLoggingExtension
{ {
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused; private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged; private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
@@ -183,7 +182,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData = LoggerMessage.Define<int, int, string>( _sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace, LogLevel.Trace,
new EventId(2028, "SendingData"), new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}"); "[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>( _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning, LogLevel.Warning,
@@ -4,9 +4,7 @@ using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions namespace CryptoExchange.Net.Logging.Extensions
{ {
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member internal static class SymbolOrderBookLoggingExtensions
public static class SymbolOrderBookLoggingExtensions
{ {
private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged; private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged;
private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting; private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting;
@@ -1,292 +0,0 @@
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);
}
}
}
+28 -6
View File
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.Objects using System.Security;
namespace CryptoExchange.Net.Objects
{ {
/// <summary> /// <summary>
/// Proxy info /// Proxy info
@@ -8,21 +10,30 @@
/// <summary> /// <summary>
/// The host address of the proxy /// The host address of the proxy
/// </summary> /// </summary>
public string Host { get; set; } public string Host { get; }
/// <summary> /// <summary>
/// The port of the proxy /// The port of the proxy
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; }
/// <summary> /// <summary>
/// The login of the proxy /// The login of the proxy
/// </summary> /// </summary>
public string? Login { get; set; } public string? Login { get; }
/// <summary> /// <summary>
/// The password of the proxy /// The password of the proxy
/// </summary> /// </summary>
public string? Password { get; set; } 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)
{
}
/// <summary> /// <summary>
/// Create new settings for a proxy /// Create new settings for a proxy
@@ -31,7 +42,18 @@
/// <param name="port">The proxy port</param> /// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param> /// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param> /// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login = null, string? password = null) 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)
{ {
Host = host; Host = host;
Port = port; Port = port;
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(byte[]? x, byte[]? y) public int Compare(byte[] x, byte[] y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
+1 -112
View File
@@ -1,5 +1,4 @@
using CryptoExchange.Net.SharedApis; using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
@@ -274,54 +273,6 @@ namespace CryptoExchange.Net.Objects
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); 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 /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
@@ -474,68 +425,6 @@ namespace CryptoExchange.Net.Objects
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, 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> /// <summary>
/// Return a copy of this result with data source set to cache /// Return a copy of this result with data source set to cache
/// </summary> /// </summary>
+1 -47
View File
@@ -68,33 +68,6 @@
Json Json
} }
/// <summary>
/// Tracker sync status
/// </summary>
public enum SyncStatus
{
/// <summary>
/// Not connected
/// </summary>
Disconnected,
/// <summary>
/// Syncing, data connection is being made
/// </summary>
Syncing,
/// <summary>
/// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment.
/// </summary>
PartiallySynced,
/// <summary>
/// Synced
/// </summary>
Synced,
/// <summary>
/// Disposed
/// </summary>
Diposed
}
/// <summary> /// <summary>
/// Status of the order book /// Status of the order book
/// </summary> /// </summary>
@@ -179,11 +152,7 @@
/// <summary> /// <summary>
/// Round to closest value /// Round to closest value
/// </summary> /// </summary>
Closest, Closest
/// <summary>
/// Round up (ceil)
/// </summary>
Up
} }
/// <summary> /// <summary>
@@ -234,19 +203,4 @@
/// </summary> /// </summary>
Cache Cache
} }
/// <summary>
/// Type of exchange
/// </summary>
public enum ExchangeType
{
/// <summary>
/// Centralized
/// </summary>
CEX,
/// <summary>
/// Decentralized
/// </summary>
DEX
}
} }
@@ -1,58 +0,0 @@
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,15 +19,19 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan? TimestampRecalculationInterval { get; set; } public TimeSpan? TimestampRecalculationInterval { get; set; }
/// <summary> /// <summary>
/// Set the values of this options on the target options /// Create a copy of this options
/// </summary> /// </summary>
public T Set<T>(T item) where T : RestApiOptions, new() /// <typeparam name="T"></typeparam>
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
{ {
item.ApiCredentials = ApiCredentials?.Copy(); return new T
item.OutputOriginalData = OutputOriginalData; {
item.AutoTimestamp = AutoTimestamp; ApiCredentials = ApiCredentials?.Copy(),
item.TimestampRecalculationInterval = TimestampRecalculationInterval; OutputOriginalData = OutputOriginalData,
return item; AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval
};
} }
} }
@@ -29,21 +29,25 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary> /// <summary>
/// Set the values of this options on the target options /// Create a copy of this options
/// </summary> /// </summary>
public T Set<T>(T item) where T : RestExchangeOptions, new() /// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : RestExchangeOptions, new()
{ {
item.OutputOriginalData = OutputOriginalData; return new T
item.AutoTimestamp = AutoTimestamp; {
item.TimestampRecalculationInterval = TimestampRecalculationInterval; OutputOriginalData = OutputOriginalData,
item.ApiCredentials = ApiCredentials?.Copy(); AutoTimestamp = AutoTimestamp,
item.Proxy = Proxy; TimestampRecalculationInterval = TimestampRecalculationInterval,
item.RequestTimeout = RequestTimeout; ApiCredentials = ApiCredentials?.Copy(),
item.RateLimiterEnabled = RateLimiterEnabled; Proxy = Proxy,
item.RateLimitingBehaviour = RateLimitingBehaviour; RequestTimeout = RequestTimeout,
item.CachingEnabled = CachingEnabled; RateLimiterEnabled = RateLimiterEnabled,
item.CachingMaxAge = CachingMaxAge; RateLimitingBehaviour = RateLimitingBehaviour,
return item; CachingEnabled = CachingEnabled,
CachingMaxAge = CachingMaxAge,
};
} }
} }
@@ -62,13 +66,15 @@ 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. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary> /// <summary>
/// Set the values of this options on the target options /// Create a copy of this options
/// </summary> /// </summary>
public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new() /// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
{ {
base.Set(target); var result = base.Copy<T>();
target.Environment = Environment; result.Environment = Environment;
return target; return result;
} }
} }
@@ -20,15 +20,19 @@ namespace CryptoExchange.Net.Objects.Options
public int? MaxSocketConnections { get; set; } public int? MaxSocketConnections { get; set; }
/// <summary> /// <summary>
/// Set the values of this options on the target options /// Create a copy of this options
/// </summary> /// </summary>
public T Set<T>(T item) where T : SocketApiOptions, new() /// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketApiOptions, new()
{ {
item.ApiCredentials = ApiCredentials?.Copy(); return new T
item.OutputOriginalData = OutputOriginalData; {
item.SocketNoDataTimeout = SocketNoDataTimeout; ApiCredentials = ApiCredentials?.Copy(),
item.MaxSocketConnections = MaxSocketConnections; OutputOriginalData = OutputOriginalData,
return item; SocketNoDataTimeout = SocketNoDataTimeout,
MaxSocketConnections = MaxSocketConnections,
};
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Sockets;
using System; using System;
namespace CryptoExchange.Net.Objects.Options namespace CryptoExchange.Net.Objects.Options
@@ -46,33 +47,29 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero; 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> /// <summary>
/// Create a copy of this options /// Create a copy of this options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <returns></returns> /// <returns></returns>
public T Set<T>(T item) where T : SocketExchangeOptions, new() public T Copy<T>() where T : SocketExchangeOptions, new()
{ {
item.ApiCredentials = ApiCredentials?.Copy(); return new T
item.OutputOriginalData = OutputOriginalData; {
item.ReconnectPolicy = ReconnectPolicy; ApiCredentials = ApiCredentials?.Copy(),
item.DelayAfterConnect = DelayAfterConnect; OutputOriginalData = OutputOriginalData,
item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket; ReconnectPolicy = ReconnectPolicy,
item.ReconnectInterval = ReconnectInterval; DelayAfterConnect = DelayAfterConnect,
item.SocketNoDataTimeout = SocketNoDataTimeout; MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget; ReconnectInterval = ReconnectInterval,
item.MaxSocketConnections = MaxSocketConnections; SocketNoDataTimeout = SocketNoDataTimeout,
item.Proxy = Proxy; SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
item.RequestTimeout = RequestTimeout; MaxSocketConnections = MaxSocketConnections,
item.RateLimitingBehaviour = RateLimitingBehaviour; Proxy = Proxy,
item.RateLimiterEnabled = RateLimiterEnabled; RequestTimeout = RequestTimeout,
return item; RateLimitingBehaviour = RateLimitingBehaviour,
RateLimiterEnabled = RateLimiterEnabled,
};
} }
} }
@@ -91,13 +88,15 @@ 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. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary> /// <summary>
/// Set the values of this options on the target options /// Create a copy of this options
/// </summary> /// </summary>
public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new() /// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
{ {
base.Set(target); var result = base.Copy<T>();
target.Environment = Environment; result.Environment = Environment;
return target; return result;
} }
} }
@@ -1,29 +0,0 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<T> where T : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public T? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}
/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
}
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(string? x, string? y) public int Compare(string x, string y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
@@ -149,27 +149,6 @@ namespace CryptoExchange.Net.Objects
Add(key, DateTimeConverter.ConvertToSeconds(value)); 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> /// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" /> /// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary> /// </summary>
@@ -187,8 +166,8 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value) public void AddEnumAsInt<T>(string key, T value)
{ {
var stringVal = EnumConverter.GetString(value)!; var stringVal = EnumConverter.GetString(value);
Add(key, int.Parse(stringVal)!); Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
} }
/// <summary> /// <summary>
@@ -1,4 +1,5 @@
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Net.Http; using System.Net.Http;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
@@ -58,16 +59,12 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public IRateLimitGuard? LimitGuard { get; set; } public IRateLimitGuard? LimitGuard { get; set; }
/// <summary> /// <summary>
/// Whether this request should never be cached /// Whether this request should never be cached
/// </summary> /// </summary>
public bool PreventCaching { get; set; } public bool PreventCaching { get; set; }
/// <summary>
/// Connection id
/// </summary>
public int? ConnectionId { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -1,5 +1,7 @@
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
@@ -58,38 +60,9 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{ {
if (!_definitions.TryGetValue(identifier, out var def)) if (!_definitions.TryGetValue(method + path, out var def))
{ {
def = new RequestDefinition(path, method) def = new RequestDefinition(path, method)
{ {
@@ -102,7 +75,7 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false PreventCaching = preventCaching ?? false
}; };
_definitions.TryAdd(identifier, def); _definitions.TryAdd(method + path, def);
} }
return def; return def;
@@ -1,5 +1,4 @@
using CryptoExchange.Net.SharedApis; using System;
using System;
namespace CryptoExchange.Net.Objects.Sockets namespace CryptoExchange.Net.Objects.Sockets
{ {
@@ -39,10 +38,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public T Data { get; set; } public T Data { get; set; }
/// <summary> internal DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
/// ctor
/// </summary>
public DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
{ {
Data = data; Data = data;
StreamId = streamId; StreamId = streamId;
@@ -89,19 +85,6 @@ namespace CryptoExchange.Net.Objects.Sockets
return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType); return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType);
} }
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange the result is for</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data)
{
return new ExchangeEvent<K>(exchange, this.As<K>(data));
}
/// <summary> /// <summary>
/// Specify the symbol /// Specify the symbol
/// </summary> /// </summary>
@@ -161,11 +144,5 @@ namespace CryptoExchange.Net.Objects.Sockets
{ {
return new CallResult<K>(default, OriginalData, error); return new CallResult<K>(default, OriginalData, error);
} }
/// <inheritdoc />
public override string ToString()
{
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}";
}
} }
} }
@@ -30,15 +30,6 @@ namespace CryptoExchange.Net.Objects.Sockets
remove => _connection.ConnectionClosed -= value; 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> /// <summary>
/// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting.
/// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the diconnect /// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the diconnect
+2 -2
View File
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow; TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{ {
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName); Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
TimeSyncState.TimeOffset = TimeSpan.Zero; TimeSyncState.TimeOffset = TimeSpan.Zero;
} }
else else
{ {
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds)); Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
TimeSyncState.TimeOffset = offset; TimeSyncState.TimeOffset = offset;
} }
} }
@@ -24,14 +24,14 @@
/// <summary> /// <summary>
/// Name of the environment /// Name of the environment
/// </summary> /// </summary>
public string Name { get; set; } public string EnvironmentName { get; init; }
/// <summary> /// <summary>
/// </summary> /// </summary>
/// <param name="name"></param> /// <param name="name"></param>
protected TradeEnvironment(string name) protected TradeEnvironment(string name)
{ {
Name = name; EnvironmentName = name;
} }
} }
} }
@@ -1,6 +1,5 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System; using System;
namespace CryptoExchange.Net.OrderBook namespace CryptoExchange.Net.OrderBook
@@ -9,14 +8,14 @@ namespace CryptoExchange.Net.OrderBook
public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions
{ {
private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor; private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor;
private readonly Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> _assetsCtor; private readonly Func<string, string, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="symbolCtor"></param> /// <param name="symbolCtor"></param>
/// <param name="assetsCtor"></param> /// <param name="assetsCtor"></param>
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> assetsCtor) public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<string, string, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
{ {
_symbolCtor = symbolCtor; _symbolCtor = symbolCtor;
_assetsCtor = assetsCtor; _assetsCtor = assetsCtor;
@@ -26,9 +25,6 @@ namespace CryptoExchange.Net.OrderBook
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options); public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options);
/// <inheritdoc /> /// <inheritdoc />
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset), options); public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(baseAsset, quoteAsset, options);
/// <inheritdoc />
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null) => _assetsCtor(symbol, options);
} }
} }
@@ -810,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
{ {
if (lastUpdateId <= LastSequenceNumber) if (lastUpdateId <= LastSequenceNumber)
{ {
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber); _logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
return; return;
} }
@@ -843,9 +843,9 @@ namespace CryptoExchange.Net.OrderBook
internal class DescComparer<T> : IComparer<T> internal class DescComparer<T> : IComparer<T>
{ {
public int Compare(T? x, T? y) public int Compare(T x, T y)
{ {
return Comparer<T>.Default.Compare(y!, x!); return Comparer<T>.Default.Compare(y, x);
} }
} }
} }
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters namespace CryptoExchange.Net.RateLimiting.Filters
{ {
@@ -20,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> definition.Authenticated == _authenticated; => definition.Authenticated == _authenticated;
} }
} }
@@ -1,6 +1,9 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Filters namespace CryptoExchange.Net.RateLimiting.Filters
{ {
@@ -21,7 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase); => string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
} }
} }
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters namespace CryptoExchange.Net.RateLimiting.Filters
{ {
@@ -21,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> _paths.Contains(definition.Path); => _paths.Contains(definition.Path);
} }
} }
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters namespace CryptoExchange.Net.RateLimiting.Filters
{ {
@@ -20,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> host == _host; => host == _host;
} }
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters namespace CryptoExchange.Net.RateLimiting.Filters
{ {
@@ -20,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> type == _type; => type == _type;
} }
} }
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System; using System;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Filters namespace CryptoExchange.Net.RateLimiting.Filters
{ {
@@ -21,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
} }
/// <inheritdoc /> /// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey) public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase); => definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
} }
} }
@@ -3,6 +3,8 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers; using CryptoExchange.Net.RateLimiting.Trackers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Guards namespace CryptoExchange.Net.RateLimiting.Guards
{ {
@@ -12,30 +14,26 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Apply guard per host /// Apply guard per host
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host); public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
/// <summary> /// <summary>
/// Apply guard per endpoint /// Apply guard per endpoint
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method); public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
/// <summary>
/// Apply guard per connection
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
/// <summary> /// <summary>
/// Apply guard per API key /// Apply guard per API key
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!); public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
/// <summary> /// <summary>
/// Apply guard per API key per endpoint /// Apply guard per API key per endpoint
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method); public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
private readonly IEnumerable<IGuardFilter> _filters; private readonly IEnumerable<IGuardFilter> _filters;
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private RateLimitWindowType _windowType; private RateLimitWindowType _windowType;
private double? _decayRate; private double? _decayRate;
private int? _connectionWeight; private int? _connectionWeight;
private readonly Func<RequestDefinition, string, string?, string> _keySelector; private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
/// <inheritdoc /> /// <inheritdoc />
public string Name => "RateLimitGuard"; public string Name => "RateLimitGuard";
@@ -62,7 +60,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param> /// <param name="connectionWeight">The weight of a new connection</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null) public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight) : this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
{ {
} }
@@ -77,7 +75,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param> /// <param name="connectionWeight">The weight of a new connection</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null) public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
{ {
_filters = filters; _filters = filters;
_trackers = new Dictionary<string, IWindowTracker>(); _trackers = new Dictionary<string, IWindowTracker>();
@@ -90,7 +88,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{ {
foreach(var filter in _filters) foreach(var filter in _filters)
{ {
@@ -110,13 +108,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current); return LimitCheck.NotNeeded;
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current); return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
} }
/// <inheritdoc /> /// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{ {
foreach (var filter in _filters) foreach (var filter in _filters)
{ {
@@ -1,6 +1,9 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using System; using System;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace CryptoExchange.Net.RateLimiting.Guards namespace CryptoExchange.Net.RateLimiting.Guards
{ {
@@ -18,35 +21,25 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "RetryAfterGuard"; public string Name => "RetryAfterGuard";
/// <inheritdoc /> /// <inheritdoc />
public string Description => $"Pause {Type} until after {After}"; public string Description => $"Pause requests until after {After}";
/// <summary> /// <summary>
/// The timestamp after which requests are allowed again /// The timestamp after which requests are allowed again
/// </summary> /// </summary>
public DateTime After { get; private set; } public DateTime After { get; private set; }
/// <summary>
/// The type of rate limit item this guard is for
/// </summary>
public RateLimitItemType Type { get; private set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="after"></param> /// <param name="after"></param>
/// <param name="type"></param> public RetryAfterGuard(DateTime after)
public RetryAfterGuard(DateTime after, RateLimitItemType type)
{ {
After = after; After = after;
Type = type;
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{ {
if (type != Type)
return LimitCheck.NotApplicable;
var dif = (After + _windowBuffer) - DateTime.UtcNow; var dif = (After + _windowBuffer) - DateTime.UtcNow;
if (dif <= TimeSpan.Zero) if (dif <= TimeSpan.Zero)
return LimitCheck.NotApplicable; return LimitCheck.NotApplicable;
@@ -55,7 +48,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{ {
return RateLimitState.NotApplied; return RateLimitState.NotApplied;
} }
@@ -3,6 +3,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers; using CryptoExchange.Net.RateLimiting.Trackers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Guards namespace CryptoExchange.Net.RateLimiting.Guards
{ {
@@ -14,19 +15,19 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Default endpoint limit /// Default endpoint limit
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method); public static Func<RequestDefinition, string, SecureString?, string> Default { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
/// <summary> /// <summary>
/// Endpoint limit per API key /// Endpoint limit per API key
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method); public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate; private readonly double? _decayRate;
private readonly int _limit; private readonly int _limit;
private readonly TimeSpan _period; private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string, string?, string> _keySelector; private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
/// <inheritdoc /> /// <inheritdoc />
public string Name => "EndpointLimitGuard"; public string Name => "EndpointLimitGuard";
@@ -42,7 +43,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
TimeSpan period, TimeSpan period,
RateLimitWindowType windowType, RateLimitWindowType windowType,
double? decayRate = null, double? decayRate = null,
Func<RequestDefinition, string, string?, string>? keySelector = null) Func<RequestDefinition, string, SecureString?, string>? keySelector = null)
{ {
_limit = limit; _limit = limit;
_period = period; _period = period;
@@ -53,7 +54,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{ {
var key = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey);
if (!_trackers.TryGetValue(key, out var tracker)) if (!_trackers.TryGetValue(key, out var tracker))
@@ -64,13 +65,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded(_limit, _period, tracker.Current); return LimitCheck.NotNeeded;
return LimitCheck.Needed(delay, _limit, _period, tracker.Current); return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
} }
/// <inheritdoc /> /// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight) public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
{ {
var key = _keySelector(definition, host, apiKey); var key = _keySelector(definition, host, apiKey);
var tracker = _trackers[key]; var tracker = _trackers[key];
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Interfaces namespace CryptoExchange.Net.RateLimiting.Interfaces
{ {
@@ -15,6 +16,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="host">The host address</param> /// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <returns>True if passed</returns> /// <returns>True if passed</returns>
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey); bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
} }
} }
@@ -1,6 +1,8 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting.Guards;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -16,11 +18,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary> /// </summary>
event Action<RateLimitEvent> RateLimitTriggered; event Action<RateLimitEvent> RateLimitTriggered;
/// <summary>
/// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying.
/// </summary>
event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary> /// <summary>
/// Add a rate limit guard /// Add a rate limit guard
/// </summary> /// </summary>
@@ -32,9 +29,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified /// Set a RetryAfter guard, can be used when a server rate limit is hit and a RetryAfter header is specified
/// </summary> /// </summary>
/// <param name="retryAfter">The time after which requests can be send again</param> /// <param name="retryAfter">The time after which requests can be send again</param>
/// <param name="type">RateLimitType</param>
/// <returns></returns> /// <returns></returns>
Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type = RateLimitItemType.Request); Task SetRetryAfterGuardAsync(DateTime retryAfter);
/// <summary> /// <summary>
/// Returns the 'retry after' timestamp if set /// Returns the 'retry after' timestamp if set
@@ -55,7 +51,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct); Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
/// <summary> /// <summary>
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail /// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
@@ -68,9 +64,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="baseAddress">The host address</param> /// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct); Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct);
} }
} }
@@ -1,4 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System.Net.Http;
using System.Security;
namespace CryptoExchange.Net.RateLimiting.Interfaces namespace CryptoExchange.Net.RateLimiting.Interfaces
{ {
@@ -26,7 +28,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <returns></returns> /// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight); LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
/// <summary> /// <summary>
/// Apply the request to this guard with the specified weight /// Apply the request to this guard with the specified weight
@@ -37,6 +39,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <returns></returns> /// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight); RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
} }
} }
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary> /// <summary>
/// No wait needed /// No wait needed
/// </summary> /// </summary>
public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current); public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
/// <summary> /// <summary>
/// Wait needed /// Wait needed
@@ -4,14 +4,10 @@ using System;
namespace CryptoExchange.Net.RateLimiting namespace CryptoExchange.Net.RateLimiting
{ {
/// <summary> /// <summary>
/// Rate limit triggered event /// Rate limit event
/// </summary> /// </summary>
public record RateLimitEvent public record RateLimitEvent
{ {
/// <summary>
/// Id of the item the limit was checked for
/// </summary>
public int ItemId { get; set; }
/// <summary> /// <summary>
/// Name of the API limit that is reached /// Name of the API limit that is reached
/// </summary> /// </summary>
@@ -56,9 +52,18 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour) /// <param name="apiLimit"></param>
/// <param name="limitDescription"></param>
/// <param name="definition"></param>
/// <param name="host"></param>
/// <param name="current"></param>
/// <param name="requestWeight"></param>
/// <param name="limit"></param>
/// <param name="timePeriod"></param>
/// <param name="delayTime"></param>
/// <param name="behaviour"></param>
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
{ {
ItemId = itemId;
ApiLimit = apiLimit; ApiLimit = apiLimit;
LimitDescription = limitDescription; LimitDescription = limitDescription;
RequestDefinition = definition; RequestDefinition = definition;
@@ -7,6 +7,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -23,8 +24,6 @@ namespace CryptoExchange.Net.RateLimiting
/// <inheritdoc /> /// <inheritdoc />
public event Action<RateLimitEvent>? RateLimitTriggered; public event Action<RateLimitEvent>? RateLimitTriggered;
/// <inheritdoc />
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -33,30 +32,22 @@ namespace CryptoExchange.Net.RateLimiting
{ {
_name = name; _name = name;
_guards = new ConcurrentBag<IRateLimitGuard>(); _guards = new ConcurrentBag<IRateLimitGuard>();
_semaphore = new SemaphoreSlim(1, 1); _semaphore = new SemaphoreSlim(1);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct) public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
}
finally finally
{ {
_waitingCount--; _waitingCount--;
if (release) _semaphore.Release();
_semaphore.Release();
} }
} }
@@ -67,34 +58,26 @@ namespace CryptoExchange.Net.RateLimiting
IRateLimitGuard guard, IRateLimitGuard guard,
RateLimitItemType type, RateLimitItemType type,
RequestDefinition definition, RequestDefinition definition,
string host, string host,
string? apiKey, SecureString? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour, RateLimitingBehaviour rateLimitingBehaviour,
CancellationToken ct) CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError());
} }
finally finally
{ {
_waitingCount--; _waitingCount--;
if (release) _semaphore.Release();
_semaphore.Release();
} }
} }
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct) private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
{ {
foreach (var guard in guards) foreach (var guard in guards)
{ {
@@ -108,7 +91,7 @@ namespace CryptoExchange.Net.RateLimiting
else else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}")); return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
} }
@@ -123,7 +106,7 @@ namespace CryptoExchange.Net.RateLimiting
else else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
@@ -136,8 +119,6 @@ namespace CryptoExchange.Net.RateLimiting
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight); var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
if (result.IsApplied) if (result.IsApplied)
{ {
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
if (type == RateLimitItemType.Connection) if (type == RateLimitItemType.Connection)
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
else else
@@ -156,7 +137,7 @@ namespace CryptoExchange.Net.RateLimiting
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task SetRetryAfterGuardAsync(DateTime retryAfter, RateLimitItemType type) public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
{ {
await _semaphore.WaitAsync().ConfigureAwait(false); await _semaphore.WaitAsync().ConfigureAwait(false);
@@ -164,7 +145,7 @@ namespace CryptoExchange.Net.RateLimiting
{ {
var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault(); var retryAfterGuard = _guards.OfType<RetryAfterGuard>().SingleOrDefault();
if (retryAfterGuard == null) if (retryAfterGuard == null)
_guards.Add(new RetryAfterGuard(retryAfter, type)); _guards.Add(new RetryAfterGuard(retryAfter));
else else
retryAfterGuard.UpdateAfter(retryAfter); retryAfterGuard.UpdateAfter(retryAfter);
} }
@@ -1,50 +0,0 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit update event
/// </summary>
public record RateLimitUpdateEvent
{
/// <summary>
/// Id of the item the limit was checked for
/// </summary>
public int ItemId { get; set; }
/// <summary>
/// Name of the API limit that is reached
/// </summary>
public string ApiLimit { get; set; } = string.Empty;
/// <summary>
/// Description of the limit that is reached
/// </summary>
public string LimitDescription { get; set; } = string.Empty;
/// <summary>
/// The current counter value
/// </summary>
public int Current { get; set; }
/// <summary>
/// The limit per time period
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// The time period the limit is for
/// </summary>
public TimeSpan? TimePeriod { get; set; }
/// <summary>
/// ctor
/// </summary>
public RateLimitUpdateEvent(int itemId, string apiLimit, string limitDescription, int current, int? limit, TimeSpan? timePeriod)
{
ItemId = itemId;
ApiLimit = apiLimit;
LimitDescription = limitDescription;
Current = current;
Limit = limit;
TimePeriod = timePeriod;
}
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.Requests
} }
/// <inheritdoc /> /// <inheritdoc />
public Uri Uri => _request.RequestUri!; public Uri Uri => _request.RequestUri;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
+18 -34
View File
@@ -11,13 +11,29 @@ namespace CryptoExchange.Net.Requests
/// </summary> /// </summary>
public class RequestFactory : IRequestFactory public class RequestFactory : IRequestFactory
{ {
private HttpClient? _httpClient; private HttpClient? _httpClient;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
if (client == null) if (client == null)
client = CreateClient(proxy, requestTimeout); {
var handler = new HttpClientHandler();
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
_httpClient = client; _httpClient = client;
} }
@@ -30,37 +46,5 @@ namespace CryptoExchange.Net.Requests
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
} }
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
{
_httpClient = CreateClient(proxy, requestTimeout);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var client = new HttpClient(handler)
{
Timeout = requestTimeout
};
return client;
}
} }
} }
@@ -1,29 +0,0 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Fee asset selection type
/// </summary>
public enum SharedFeeAssetType
{
/// <summary>
/// Fee is always in the base asset
/// </summary>
BaseAsset,
/// <summary>
/// Fee is always in the quote asset
/// </summary>
QuoteAsset,
/// <summary>
/// Fee is always in the input asset
/// </summary>
InputAsset,
/// <summary>
/// Fee is always in the output asset
/// </summary>
OutputAsset,
/// <summary>
/// Fee is variable
/// </summary>
Variable
}
}

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