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

Compare commits

..

90 Commits

Author SHA1 Message Date
Jkorf 02bbd37bb6 Updated to version 8.7.3 2025-02-05 09:15:25 +01:00
Jkorf 1bbbec7f2b Fixed issue with serialization of nullable types in System.Text.Json ArrayConverter 2025-02-05 09:12:12 +01:00
Jkorf 0262f04913 Added handling of negative number DateTime deserialization to default 2025-02-05 08:25:59 +01:00
Jkorf fd1ec17d72 Fix for unnecessary error message in logging when closing connection 2025-02-04 08:28:48 +01:00
Jkorf 4bdad7fe0c Updated SharedSymbol from class to record 2025-02-04 08:28:21 +01:00
Jkorf 74f73dc790 Updated to version 8.7.2 2025-01-27 13:24:08 +01:00
Jkorf 0527a8a76e Some small fixes in the System.Text.Json ArrayConverter, added support for flags in EnumConverter 2025-01-27 11:52:07 +01:00
Jkorf c693eb8c02 Updated to version 8.7.1 2025-01-24 08:42:08 +01:00
Jkorf 3eb28c7fed Added HyperLiquid referral 2025-01-23 09:35:44 +01:00
JKorf 618c4922b9 Added Authenticated property to IBaseApiClient interface 2025-01-22 19:11:38 +01:00
Jkorf c81b15861d Updated examples and docs with HyperLiquid references 2025-01-21 15:24:23 +01:00
Jkorf 4a5832cccd Updated to version 8.7.0 2025-01-21 14:02:42 +01:00
Jkorf 4e47c4cbdf Updated CheckForMissingInterfaces test 2025-01-21 14:00:30 +01:00
Jkorf 2af1520ecc Added PriceSignificationFigures to SharedSpotSymbol model 2025-01-21 14:00:08 +01:00
Jkorf cf397af3ab Added GetMillisecondTimestampLong helper method to AuthenticationProvider 2025-01-21 13:47:05 +01:00
JKorf a1479705e2 Fixed typo 2025-01-13 17:49:38 +01:00
Jkorf 175e23f110 Updated to version 8.6.1 2025-01-09 16:25:11 +01:00
Jkorf 9b7019ded2 Removed websocket Error callback when exception is expected 2025-01-09 16:23:34 +01:00
Jkorf 7904aa9ba7 Fixed websocket connection getting stuck after a ping frame timeout, removed unnecessary type restraints on RestApiClient.SendAsync methods 2025-01-09 16:18:13 +01:00
Jkorf 3fe6db589f Updated to version 8.6.0 2025-01-07 13:25:50 +01:00
Jkorf 625dccbbe4 Added ExchangeType enum, some small improvements 2025-01-07 13:22:16 +01:00
Jkorf e650771d16 Added response headers parameter to RestApiClient.TryParseError method, added check for ServerRateLimitError on the result 2025-01-07 10:19:37 +01:00
Jkorf 3dad28b19d Added IFeeRestClient to service registration 2025-01-07 08:59:51 +01:00
Jkorf 2b9fda985e Add support for passing weight to apply to an individual ratelimit guard 2025-01-07 08:35:06 +01:00
JKorf ff8759409b Use Convert.ToHexString if available 2025-01-06 21:38:42 +01:00
JKorf 0d9627c13f Changed socket no data reconnect message to LogLevel Warning 2024-12-23 20:03:56 +01:00
Jkorf 0179fd7e2a Fixed workflow automated tests 2024-12-23 14:43:23 +01:00
Jkorf b8d0b0cf95 Workflow fix 2024-12-23 14:36:18 +01:00
Jkorf 73c42bd452 Updated to version 8.5.0 2024-12-23 14:25:39 +01:00
Jkorf 290be7f5e0 Added net9.0 build target, added KeepAliveTimeout for websocket connections 2024-12-23 14:14:47 +01:00
Jan Korf 0be1bb16e3 Feature/update settings (#225)
Added SetOptions method to update client settings
Added SocketConnection parameter to PeriodicQuery callback
Added setting of DefaultProxyCredentials on HttpClient instance when client is not provided by DI
Added support for overriding request time out per request
Changed max wait time for close handshake response from 5 seconds to 1 second
Fixed exception in trade tracker when there is no data in the initial snapshot
2024-12-23 08:49:58 +01:00
Jkorf 8605196390 Updated to version 8.4.5 2024-12-20 15:25:41 +01:00
Jkorf 460dd97537 Added EmptyArrayObjectConverter, added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor ctor 2024-12-20 15:21:21 +01:00
JKorf 1ec5984fad Updated to version 8.4.4 2024-12-08 10:16:19 +01:00
JKorf 8260c2661d Changed JsonConverterCtorAttribute to use type parameter instead of generic parameter to support .net framework 2024-12-08 10:12:39 +01:00
Jkorf 591c1dd405 docs 2024-12-04 10:44:35 +01:00
Jkorf 0164cdfcc4 docs 2024-12-04 09:15:23 +01:00
Jkorf 23a6cfff87 docs 2024-12-04 09:07:59 +01:00
Jkorf fdcdb90a5f Added XT reference 2024-12-04 08:58:46 +01:00
Jkorf 0b7107401f Updated to version 8.4.3 2024-12-03 09:59:36 +01:00
JKorf 06add65354 Fixed KlineTracker update handling 2024-12-02 21:46:30 +01:00
Jkorf 773d288497 Updated to version 8.4.2 2024-12-02 14:39:27 +01:00
Jkorf fd4e8da938 Removed special characters in ClientOrderIdSeperator to adhere to field content rules 2024-12-02 14:38:22 +01:00
Jkorf 271743b669 Updated to version 8.4.1 2024-12-02 13:15:31 +01:00
Jkorf f4797caf37 Added replace converter, added library helpers class 2024-12-02 13:13:56 +01:00
Jkorf 62c9769c72 Updated to version 8.4.0 2024-11-28 14:24:00 +01:00
Jkorf 92d7bc1e2e Added GetFeesAsync Shared REST client support, Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions 2024-11-28 14:18:09 +01:00
Jkorf 99e4f96f63 Updated some testing code 2024-11-27 13:07:26 +01:00
Jkorf 94d8afe149 Updated package dependency versions 2024-11-27 13:07:05 +01:00
Jkorf 90ad59c63a Added comma split enum string json converter 2024-11-22 16:20:09 +01:00
Jkorf c2273edfaa Added library options class 2024-11-20 09:52:38 +01:00
Jkorf 236283f4dd Update example-config.json 2024-11-19 14:53:58 +01:00
Jkorf b66f12ff75 Updated to version 8.3.0 2024-11-19 11:52:46 +01:00
Jkorf 0403384beb Fixed warnings 2024-11-19 11:50:55 +01:00
Jan Korf 7d7bc35869 Client Configuration (#219)
Added support for IOptions injection, allowing options to be read from IConfiguration
Small refactor on client options internals
Updated HttpClient to be static field to be
2024-11-19 11:44:30 +01:00
Jkorf 48797038be Added rate limit update event 2024-11-13 14:29:43 +01:00
Jkorf d21792d04c Added handling of Infinity values in decimal converter 2024-11-13 11:39:55 +01:00
Jkorf 8414e9d94f Fixed concurrency issue when unsubscribing websocket subscription during reconnection 2024-11-12 16:21:15 +01:00
Jkorf ab0243445d Updated docs and examples, added WhiteBit reference 2024-11-07 11:39:44 +01:00
Jkorf f2cf70b02f Updated to version 8.2.0 2024-11-06 14:00:23 +01:00
Jkorf 9ff417bba8 Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token 2024-11-06 13:56:33 +01:00
Jkorf 6b43d08a4d Added support for not allowing duplicate subscription topics on the same websocket connection 2024-11-06 11:39:11 +01:00
Jkorf 39bf7fe9b9 Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T> 2024-11-06 11:20:37 +01:00
Jkorf b5893c3b60 Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable 2024-11-06 11:20:11 +01:00
Jkorf 15657ba683 Updated to version 8.1.1 2024-11-01 10:38:30 +01:00
Jkorf 1aed9f0c67 Fixed System.Text.Json ArrayConverter not passing serializer options to nested deserialization, fixed creating new serializer options each time a JsonConverter attribute is encountered 2024-11-01 10:34:07 +01:00
Jkorf 17f1560310 Fixed socket connections trying to authenticated connection when it's marked as dedicated request connection even when no authentication is needed 2024-11-01 09:38:01 +01:00
Jkorf 41de0a3150 Update index.html 2024-10-28 16:14:54 +01:00
Jkorf 3e410be611 Update index.html 2024-10-28 16:11:01 +01:00
Jkorf be75449e4a Updated examples, added trackers example 2024-10-28 15:38:25 +01:00
Jkorf b1b05c8f6b Added catch around HttpClientHandler.AutomaticDecompression setting as it's not support on Blazor WASM 2024-10-28 13:41:58 +01:00
Jkorf a0e588c3de Updated to version 8.1.0 2024-10-28 10:44:45 +01:00
Jan Korf 9e86a08327 Trackers (#218)
Fix for intermittently failing rate limiting test
Added ConnectionId to RequestDefinition to correctly handle connection and path rate limiting configuration
Added ValidateMessage method to websocket Query object to filter messages even though it is matched to the query based on the  ListenIdentifier
Added KlineTracker and TradeTracker implementation
2024-10-28 10:36:19 +01:00
Jkorf ed007b5272 Added overload for Create method in OrderBookFactory using SharedSymbol 2024-10-23 14:04:10 +02:00
Jkorf bdd5526244 Added Side to SharedTrade model 2024-10-23 14:01:17 +02:00
Jkorf ce35e30688 Updated documentation and examples 2024-10-22 16:20:27 +02:00
Jkorf b40f72b1b0 Added Crypto.com reference 2024-10-22 15:37:56 +02:00
Jkorf 31a6cf285b Doc fix 2024-10-22 11:56:59 +02:00
Jkorf 1842f4fda0 Set ApiCredentials in the client specific options to prevent unknown client credentials when using SetApiCredentials method on client 2024-10-22 10:15:17 +02:00
Jkorf 7a58902ab6 Made SharedFuturesTicker last/high/low price properties nullable 2024-10-22 09:54:44 +02:00
Jkorf 3cb91296ca Added DoHandleReset method for websocket subscriptions 2024-10-22 09:47:14 +02:00
Jkorf 130ed40580 Comment 2024-10-21 16:29:56 +02:00
Jkorf 94cb2caf0b Added System.Text.Json ArrayConverter Write implementation 2024-10-15 10:51:19 +02:00
Jkorf 917d060827 Updated to version 8.0.3 2024-10-14 14:13:38 +02:00
JKorf c58bc2be07 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2024-10-12 13:06:28 +02:00
JKorf ff3356e2b4 Added Authenticated property on base client and shared client 2024-10-12 13:06:22 +02:00
Jkorf 79434c7be5 Implemented GetValues System.Text.Json in message accessor 2024-10-11 16:02:17 +02:00
Jkorf 168dabc11f Added fallback for unparsable value in System.Text.Json NumberStringConverter 2024-10-09 15:42:11 +02:00
Jkorf 71ee263683 Added support for duplicate array indexes in System.Text.Json ArrayConverter 2024-10-09 15:41:43 +02:00
Jkorf 7239b9c289 docs 2024-10-09 10:25:57 +02:00
127 changed files with 5347 additions and 561 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 8.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization;
namespace CryptoExchange.Net.UnitTests
@@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
}
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
}
}
+21 -5
View File
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
[Test]
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
}
@@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
Environment = new TestEnvironment("test", "https://test.com")
};
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary>
/// The default receive window for requests
/// </summary>
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy()
internal TestClientOptions Set(TestClientOptions targetOptions)
{
var options = Copy<TestClientOptions>();
options.Api1Options = Api1Options.Copy<RestApiOptions>();
options.Api2Options = Api2Options.Copy<RestApiOptions>();
return options;
targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions;
}
}
}
@@ -302,7 +302,7 @@ namespace CryptoExchange.Net.UnitTests
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
@@ -5,6 +5,8 @@ using NUnit.Framework;
using System;
using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
namespace CryptoExchange.Net.UnitTests
{
@@ -242,6 +244,44 @@ namespace CryptoExchange.Net.UnitTests
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
@@ -281,4 +321,42 @@ namespace CryptoExchange.Net.UnitTests
[JsonConverter(typeof(BoolConverter))]
public bool Value { get; set; }
}
[JsonConverter(typeof(ArrayConverter))]
record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string Prop3 { get; set; }
[ArrayProperty(2)]
public string Prop3Again { get; set; }
[ArrayProperty(3)]
public string Prop4 { get; set; }
[ArrayProperty(4)]
public Test2 Prop5 { get; set; }
[ArrayProperty(5)]
public Test3 Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))]
public TestEnum? Prop7 { get; set; }
}
[JsonConverter(typeof(ArrayConverter))]
record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string Prop22 { get; set; }
}
record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string Prop32 { get; set; }
}
}
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test")
{
var options = TestClientOptions.Default.Copy();
var options = new TestClientOptions();
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
@@ -16,6 +16,7 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -24,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
}
Initialize(options.Value);
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
using Moq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -22,25 +23,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public TestSubSocketClient SubClient { get; }
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
optionsFunc(options);
Initialize(options);
Initialize(options.Value);
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
@@ -70,7 +66,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
Environment = new TestEnvironment("Live", "https://test.test")
};
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
}
public class TestSubSocketClient : SocketApiClient
@@ -1,4 +1,4 @@
#if !NETSTANDARD2_1
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
using System;
@@ -13,39 +13,30 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// The api key / label to authenticate requests
/// </summary>
public string Key { get; }
public string Key { get; set; }
/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; }
public string Secret { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; }
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
CredentialType = credentialType;
Key = key;
Secret = secret;
}
@@ -65,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result)
@@ -75,11 +66,9 @@ namespace CryptoExchange.Net.Authentication
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key;
Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(key, secret);
}
}
}
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key;
public string ApiKey => _credentials.Key!;
/// <summary>
/// ctor
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Secret == null)
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials;
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{
#if NETSTANDARD2_1_OR_GREATER
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key
var key = _credentials.Secret!
.Replace("\n", "")
@@ -403,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
protected static string BytesToHexString(byte[] buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2");
return result;
#endif
}
/// <summary>
@@ -439,6 +443,16 @@ namespace CryptoExchange.Net.Authentication
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>
/// Return the serialized request body
/// </summary>
+1 -1
View File
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
_cache.TryGetValue(key, out CacheItem value);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
@@ -38,6 +38,9 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
/// <summary>
/// Api options
/// </summary>
@@ -83,10 +86,22 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
ApiOptions.ApiCredentials = credentials;
if (credentials != null)
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>
/// Dispose
/// </summary>
+12 -2
View File
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Version of the CryptoExchange.Net base library
/// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version;
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary>
/// Version of the client implementation
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Clients
lock(_versionLock)
{
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version;
_exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion;
}
@@ -109,6 +109,16 @@ namespace CryptoExchange.Net.Clients
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
+43 -16
View File
@@ -154,6 +154,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
@@ -161,7 +162,8 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
int? weight = null,
int? weightSingleLimiter = null)
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
@@ -171,7 +173,8 @@ namespace CryptoExchange.Net.Clients
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken,
additionalHeaders,
weight);
weight,
weightSingleLimiter);
}
/// <summary>
@@ -185,6 +188,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
@@ -193,7 +197,8 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
int? weight = null,
int? weightSingleLimiter = null)
{
string? cacheKey = null;
if (ShouldCache(definition))
@@ -217,7 +222,7 @@ namespace CryptoExchange.Net.Clients
currentTry++;
var requestId = ExchangeHelpers.NextId();
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter).ConfigureAwait(false);
if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!);
@@ -258,6 +263,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync(
@@ -266,10 +272,9 @@ namespace CryptoExchange.Net.Clients
RequestDefinition definition,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
int? weight = null,
int? weightSingleLimiter = null)
{
var requestWeight = weight ?? definition.Weight;
// Time sync
if (definition.Authenticated)
{
@@ -295,6 +300,7 @@ namespace CryptoExchange.Net.Clients
}
// Rate limiting
var requestWeight = weight ?? definition.Weight;
if (requestWeight != 0)
{
if (definition.RateLimitGate == null)
@@ -316,7 +322,8 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled)
{
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
if (!limitResult)
return new CallResult(limitResult.Error!);
}
@@ -617,7 +624,7 @@ namespace CryptoExchange.Net.Clients
paramString = $" with request body '{request.Content}'";
var headers = request.GetHeaders();
if (headers.Any())
if (headers.Count != 0)
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++;
@@ -693,10 +700,21 @@ namespace CryptoExchange.Net.Clients
}
// Json response received
var parsedError = TryParseError(accessor);
var parsedError = TryParseError(response.ResponseHeaders, accessor);
if (parsedError != null)
{
if (parsedError is ServerRateLimitError rateError)
{
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
}
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
@@ -730,12 +748,13 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
/// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null
/// </summary>
/// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null;
protected virtual Error? TryParseError(IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor) => null;
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@@ -752,7 +771,7 @@ namespace CryptoExchange.Net.Clients
// Only retry once
return false;
if ((int?)callResult.ResponseStatusCode == 429
if (callResult.Error is ServerRateLimitError
&& ClientOptions.RateLimiterEnabled
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
&& gate != null)
@@ -807,7 +826,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InUri)
{
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>();
@@ -889,8 +908,8 @@ namespace CryptoExchange.Net.Clients
{
// Write the parameters as json in the body
string stringData;
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = CreateSerializer().Serialize(value);
else
stringData = CreateSerializer().Serialize(parameters);
request.SetContent(stringData, contentType);
@@ -961,6 +980,14 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
+47 -10
View File
@@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
/// <summary>
/// Whether to allow multiple subscriptions with the same topic on the same connection
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <inheritdoc />
public double IncomingKbps
{
@@ -153,7 +158,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
@@ -211,7 +216,7 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
@@ -403,7 +408,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = GetAuthenticationRequest(socket);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@@ -417,9 +422,10 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!;
}
_logger.Authenticated(socket.SocketId);
}
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true;
return new CallResult(null);
}
@@ -428,7 +434,7 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
@@ -478,23 +484,28 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
{
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected);
SocketConnection connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
}
else
{
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
}
if (connection != null)
@@ -519,7 +530,14 @@ namespace CryptoExchange.Net.Clients
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
if (dedicatedRequestConnection)
{
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
{
IsDedicatedRequestConnection = dedicatedRequestConnection,
Authenticated = authenticated
};
}
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
@@ -652,7 +670,7 @@ namespace CryptoExchange.Net.Clients
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection))
tasks.Add(connection.CloseAsync());
}
@@ -693,6 +711,25 @@ namespace CryptoExchange.Net.Clients
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>
/// Log the current state of connections and subscriptions
/// </summary>
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType);
return ParseObject(arr, result!, objectType);
}
private static object ParseObject(JArray arr, object result, Type objectType)
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
var count = 0;
if (innerArray.Count == 0)
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
property.SetValue(result, arrayResult);
}
else if (innerArray[0].Type == JTokenType.Array)
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
foreach (var obj in innerArray)
{
var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
count++;
}
property.SetValue(result, arrayResult);
}
else
{
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
property.SetValue(result, arrayResult);
}
continue;
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
object? value;
if (converterAttribute != null)
{
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
}
else if (conversionAttribute != null)
{
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
}
else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
&& (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{
var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value));
else
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
}
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
}
}
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{
try
{
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch (OverflowException)
{
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{
try
{
var value = reader.Value!.ToString();
var value = reader.Value!.ToString()!;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch (OverflowException)
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
/// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var value = reader.Value?.ToString().ToLower().Trim();
var value = reader.Value?.ToString()!.ToLower().Trim();
if (value == null || value == "")
{
if (Nullable.GetUnderlyingType(objectType) != null)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader);
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
private class ArrayPropertyInfo
@@ -38,12 +38,71 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private class ArrayConverterInner<T> : JsonConverter<T>
{
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// TODO
throw new NotImplementedException();
if (value == null)
{
writer.WriteNullValue();
return;
}
writer.WriteStartArray();
var valueType = value.GetType();
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
typeAttributes = CacheTypeAttributes(valueType);
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
{
writer.WriteNullValue();
last += 1;
}
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null)
{
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter);
}
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if(prop.TargetType.IsEnum)
writer.WriteStringValue(EnumConverter.GetString(objValue));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
writer.WriteEndArray();
}
/// <inheritdoc />
@@ -52,8 +111,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = Activator.CreateInstance(typeToConvert);
return (T)ParseObject(ref reader, result, typeToConvert);
var result = Activator.CreateInstance(typeToConvert)!;
return (T)ParseObject(ref reader, result, typeToConvert, options);
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
@@ -71,7 +143,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
});
}
@@ -80,7 +152,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return attributes;
}
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
@@ -94,42 +166,58 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray)
break;
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
if (attribute == null)
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{
index++;
continue;
}
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverterType != null)
foreach (var attribute in indexAttributes)
{
// Has JsonConverter attribute
var options = new JsonSerializerOptions();
options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType));
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType);
}
else
{
value = reader.TokenType switch
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverterType != null)
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
{
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
newOptions = new JsonSerializerOptions
{
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
Converters = { converter },
};
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
}
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
}
else if (attribute.DefaultDeserialization)
{
// Use default deserialization
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters);
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
index++;
}
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
try
{
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture);
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
private class BoolConverterInner<T> : JsonConverter<T>
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma seperated enum values
/// </summary>
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
{
/// <inheritdoc />
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
}
}
}
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType);
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
private class DateTimeConverterInner<T> : JsonConverter<T>
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number)
{
var longValue = reader.GetDouble();
if (longValue == 0 || longValue == -1)
if (longValue == 0 || longValue < 0)
return default;
return ParseFromDouble(longValue);
@@ -74,7 +74,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
}
else
{
var dtValue = (DateTime)(object)value;
@@ -19,9 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try
{
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
@@ -0,0 +1,37 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter mapping to an object but also handles when an empty array is send
/// </summary>
/// <typeparam name="T"></typeparam>
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
{
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
/// <inheritdoc />
public override T? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.StartArray:
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
return default;
case JsonTokenType.StartObject:
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
};
return default;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, (object?)value, options);
}
}
@@ -172,6 +172,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return true;
}
if (objectType.IsDefined(typeof(FlagsAttribute)))
{
var intValue = int.Parse(value);
result = Enum.ToObject(objectType, intValue);
return true;
}
try
{
// If no explicit mapping is found try to parse string
@@ -0,0 +1,31 @@
using System;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute for allowing specifying a JsonConverter with constructor parameters
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConverterCtorAttribute : JsonConverterAttribute
{
private readonly object[] _parameters;
private readonly Type _type;
/// <summary>
/// ctor
/// </summary>
public JsonConverterCtorAttribute(Type type, params object[] parameters)
{
_type = type;
_parameters = parameters;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert)
{
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
}
}
}
@@ -23,7 +23,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return reader.GetDecimal().ToString();
}
return reader.GetString();
try
{
return reader.GetString();
}
catch (Exception)
{
return null;
}
}
/// <inheritdoc />
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Replace a value on a string property
/// </summary>
public class ReplaceConverter : JsonConverter<string>
{
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{
_replacementSets = replaceSets.Select(x =>
{
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
if (split.Length != 2)
throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
}
@@ -21,6 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc />
public bool IsJson { get; set; }
@@ -31,6 +32,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{
@@ -42,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try
{
var result = _document.Deserialize(type, _serializerOptions);
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
return new CallResult<object>(result!);
}
catch (JsonException ex)
@@ -50,6 +66,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
catch (Exception ex)
{
var info = $"Deserialize unknown Exception: {ex.Message}";
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
}
/// <inheritdoc />
@@ -60,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try
{
var result = _document.Deserialize<T>(_serializerOptions);
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
return new CallResult<T>(result!);
}
catch (JsonException ex)
@@ -121,7 +142,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return default;
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
{
try
{
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
}
catch { }
return default;
}
if (typeof(T) == typeof(string))
{
@@ -133,7 +161,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path) => throw new NotImplementedException();
public List<T?>? GetValues<T>(MessagePath path)
{
if (!IsJson)
throw new InvalidOperationException("Can't access json data on non-json message");
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Value.ValueKind != JsonValueKind.Array)
return default;
return value.Value.Deserialize<List<T>>()!;
}
private JsonElement? GetPathNode(MessagePath path)
{
@@ -198,6 +239,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(): base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream)
{
@@ -261,6 +316,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor() : base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data)
{
+11 -10
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>8.0.2</PackageVersion>
<AssemblyVersion>8.0.2</AssemblyVersion>
<FileVersion>8.0.2</FileVersion>
<PackageVersion>8.7.3</PackageVersion>
<AssemblyVersion>8.7.3</AssemblyVersion>
<FileVersion>8.7.3</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -20,7 +20,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
@@ -48,16 +48,17 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
</ItemGroup>
</Project>
+2 -1
View File
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net
{
var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else
@@ -261,6 +261,7 @@ namespace CryptoExchange.Net
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)
{
+5 -3
View File
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net
{
if (serializationType == ArrayParametersSerialization.Array)
{
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
}
else if (serializationType == ArrayParametersSerialization.MultipleValues)
{
@@ -111,7 +111,7 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
}
}
return formData.ToString();
return formData.ToString()!;
}
/// <summary>
@@ -366,7 +366,7 @@ namespace CryptoExchange.Net
{
using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream);
@@ -435,6 +435,8 @@ namespace CryptoExchange.Net
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)!);
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
string BaseAddress { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
@@ -31,5 +37,12 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api credentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
}
}
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.Interfaces
@@ -23,5 +24,12 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="options">Options for the order book</param>
/// <returns></returns>
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null);
/// <summary>
/// Create a new order book by base and quote asset names
/// </summary>
/// <param name="symbol">Symbol</param>
/// <param name="options">Options for the order book</param>
/// <returns></returns>
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null);
}
}
@@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
}
}
@@ -62,7 +62,7 @@ namespace CryptoExchange.Net.Interfaces
Task UnsubscribeAsync(UpdateSubscription subscription);
/// <summary>
/// Prepare connections which can subsequently be used for sending websocket requests.
/// Prepare connections which can subsequently be used for sending websocket requests. Note that this is not required. If not prepared it will be initialized at the first websocket request.
/// </summary>
/// <returns></returns>
Task<CallResult> PrepareConnectionsAsync();
@@ -93,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <returns></returns>
Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
}
}
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net
{
/// <summary>
/// Helpers for client libraries
/// </summary>
public static class LibraryHelpers
{
/// <summary>
/// Client order id 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;
}
}
}
@@ -35,6 +35,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
static CryptoExchangeWebSocketClientLoggingExtension()
{
@@ -169,7 +170,7 @@ namespace CryptoExchange.Net.Logging.Extensions
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
_noDataReceiveTimoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
LogLevel.Debug,
LogLevel.Warning,
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
@@ -180,9 +181,14 @@ namespace CryptoExchange.Net.Logging.Extensions
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(1028, "SocketProcessingStateChanged"),
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(
@@ -358,5 +364,11 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
}
public static void SocketPingTimeout(
this ILogger logger, int socketId)
{
_socketPingTimeout(logger, socketId, null);
}
}
}
@@ -183,7 +183,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending messsage: {Data}");
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
@@ -0,0 +1,292 @@
using System;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Logging.Extensions
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class TrackerLoggingExtensions
{
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _klineTrackerStatusChanged;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarting;
private static readonly Action<ILogger, string, string, Exception?> _klineTrackerStartFailed;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarted;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopping;
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopped;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerInitialDataSet;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineUpdated;
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineAdded;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionLost;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionClosed;
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionRestored;
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _tradeTrackerStatusChanged;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarting;
private static readonly Action<ILogger, string, string, Exception?> _tradeTrackerStartFailed;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarted;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopping;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopped;
private static readonly Action<ILogger, string, int, long, Exception?> _tradeTrackerInitialDataSet;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotSkip;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotApplied;
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerTradeAdded;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionLost;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionClosed;
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionRestored;
static TrackerLoggingExtensions()
{
_klineTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug,
new EventId(6001, "KlineTrackerStatusChanged"),
"Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
_klineTrackerStarting = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6002, "KlineTrackerStarting"),
"Kline tracker for {Symbol} starting");
_klineTrackerStartFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(6003, "KlineTrackerStartFailed"),
"Kline tracker for {Symbol} failed to start: {Error}");
_klineTrackerStarted = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6004, "KlineTrackerStarted"),
"Kline tracker for {Symbol} started");
_klineTrackerStopping = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6005, "KlineTrackerStopping"),
"Kline tracker for {Symbol} stopping");
_klineTrackerStopped = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6006, "KlineTrackerStopped"),
"Kline tracker for {Symbol} stopped");
_klineTrackerInitialDataSet = LoggerMessage.Define<string, DateTime>(
LogLevel.Debug,
new EventId(6007, "KlineTrackerInitialDataSet"),
"Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}");
_klineTrackerKlineUpdated = LoggerMessage.Define<string, DateTime>(
LogLevel.Trace,
new EventId(6008, "KlineTrackerKlineUpdated"),
"Kline tracker for {Symbol} kline updated for open time: {LastTime}");
_klineTrackerKlineAdded = LoggerMessage.Define<string, DateTime>(
LogLevel.Trace,
new EventId(6009, "KlineTrackerKlineAdded"),
"Kline tracker for {Symbol} new kline for open time: {LastTime}");
_klineTrackerConnectionLost = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6010, "KlineTrackerConnectionLost"),
"Kline tracker for {Symbol} connection lost");
_klineTrackerConnectionClosed = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6011, "KlineTrackerConnectionClosed"),
"Kline tracker for {Symbol} disconnected");
_klineTrackerConnectionRestored = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6012, "KlineTrackerConnectionRestored"),
"Kline tracker for {Symbol} successfully resynchronized");
_tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
LogLevel.Debug,
new EventId(6013, "KlineTrackerStatusChanged"),
"Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
_tradeTrackerStarting = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6014, "KlineTrackerStarting"),
"Trade tracker for {Symbol} starting");
_tradeTrackerStartFailed = LoggerMessage.Define<string, string>(
LogLevel.Warning,
new EventId(6015, "KlineTrackerStartFailed"),
"Trade tracker for {Symbol} failed to start: {Error}");
_tradeTrackerStarted = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6016, "KlineTrackerStarted"),
"Trade tracker for {Symbol} started");
_tradeTrackerStopping = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(6017, "KlineTrackerStopping"),
"Trade tracker for {Symbol} stopping");
_tradeTrackerStopped = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6018, "KlineTrackerStopped"),
"Trade tracker for {Symbol} stopped");
_tradeTrackerInitialDataSet = LoggerMessage.Define<string, int, long>(
LogLevel.Debug,
new EventId(6019, "TradeTrackerInitialDataSet"),
"Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}");
_tradeTrackerPreSnapshotSkip = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6020, "TradeTrackerPreSnapshotSkip"),
"Trade tracker for {Symbol} skipping {Id}, already in snapshot");
_tradeTrackerPreSnapshotApplied = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6021, "TradeTrackerPreSnapshotApplied"),
"Trade tracker for {Symbol} adding {Id} from pre-snapshot");
_tradeTrackerTradeAdded = LoggerMessage.Define<string, long>(
LogLevel.Trace,
new EventId(6022, "TradeTrackerTradeAdded"),
"Trade tracker for {Symbol} adding trade {Id}");
_tradeTrackerConnectionLost = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6023, "TradeTrackerConnectionLost"),
"Trade tracker for {Symbol} connection lost");
_tradeTrackerConnectionClosed = LoggerMessage.Define<string>(
LogLevel.Warning,
new EventId(6024, "TradeTrackerConnectionClosed"),
"Trade tracker for {Symbol} disconnected");
_tradeTrackerConnectionRestored = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(6025, "TradeTrackerConnectionRestored"),
"Trade tracker for {Symbol} successfully resynchronized");
}
public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
{
_klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
}
public static void KlineTrackerStarting(this ILogger logger, string symbol)
{
_klineTrackerStarting(logger, symbol, null);
}
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
{
_klineTrackerStartFailed(logger, symbol, error, null);
}
public static void KlineTrackerStarted(this ILogger logger, string symbol)
{
_klineTrackerStarted(logger, symbol, null);
}
public static void KlineTrackerStopping(this ILogger logger, string symbol)
{
_klineTrackerStopping(logger, symbol, null);
}
public static void KlineTrackerStopped(this ILogger logger, string symbol)
{
_klineTrackerStopped(logger, symbol, null);
}
public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerInitialDataSet(logger, symbol, lastTime, null);
}
public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerKlineUpdated(logger, symbol, lastTime, null);
}
public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime)
{
_klineTrackerKlineAdded(logger, symbol, lastTime, null);
}
public static void KlineTrackerConnectionLost(this ILogger logger, string symbol)
{
_klineTrackerConnectionLost(logger, symbol, null);
}
public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol)
{
_klineTrackerConnectionClosed(logger, symbol, null);
}
public static void KlineTrackerConnectionRestored(this ILogger logger, string symbol)
{
_klineTrackerConnectionRestored(logger, symbol, null);
}
public static void TradeTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
{
_tradeTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
}
public static void TradeTrackerStarting(this ILogger logger, string symbol)
{
_tradeTrackerStarting(logger, symbol, null);
}
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
{
_tradeTrackerStartFailed(logger, symbol, error, null);
}
public static void TradeTrackerStarted(this ILogger logger, string symbol)
{
_tradeTrackerStarted(logger, symbol, null);
}
public static void TradeTrackerStopping(this ILogger logger, string symbol)
{
_tradeTrackerStopping(logger, symbol, null);
}
public static void TradeTrackerStopped(this ILogger logger, string symbol)
{
_tradeTrackerStopped(logger, symbol, null);
}
public static void TradeTrackerInitialDataSet(this ILogger logger, string symbol, int count, long lastId)
{
_tradeTrackerInitialDataSet(logger, symbol, count, lastId, null);
}
public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null);
}
public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null);
}
public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId)
{
_tradeTrackerTradeAdded(logger, symbol, lastId, null);
}
public static void TradeTrackerConnectionLost(this ILogger logger, string symbol)
{
_tradeTrackerConnectionLost(logger, symbol, null);
}
public static void TradeTrackerConnectionClosed(this ILogger logger, string symbol)
{
_tradeTrackerConnectionClosed(logger, symbol, null);
}
public static void TradeTrackerConnectionRestored(this ILogger logger, string symbol)
{
_tradeTrackerConnectionRestored(logger, symbol, null);
}
}
}
+5 -14
View File
@@ -8,30 +8,21 @@
/// <summary>
/// The host address of the proxy
/// </summary>
public string Host { get; }
public string Host { get; set; }
/// <summary>
/// The port of the proxy
/// </summary>
public int Port { get; }
public int Port { get; set; }
/// <summary>
/// The login of the proxy
/// </summary>
public string? Login { get; }
public string? Login { get; set; }
/// <summary>
/// The password of the proxy
/// </summary>
public string? Password { get; }
/// <summary>
/// Create new settings for a proxy
/// </summary>
/// <param name="host">The proxy hostname/ip</param>
/// <param name="port">The proxy port</param>
public ApiProxy(string host, int port): this(host, port, null, null)
{
}
public string? Password { get; set; }
/// <summary>
/// Create new settings for a proxy
@@ -40,7 +31,7 @@
/// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, string? password)
public ApiProxy(string host, int port, string? login = null, string? password = null)
{
Host = host;
Port = port;
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public int Compare(byte[] x, byte[] y)
public int Compare(byte[]? x, byte[]? y)
{
// Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0;
+41
View File
@@ -68,6 +68,33 @@
Json
}
/// <summary>
/// Tracker sync status
/// </summary>
public enum SyncStatus
{
/// <summary>
/// Not connected
/// </summary>
Disconnected,
/// <summary>
/// Syncing, data connection is being made
/// </summary>
Syncing,
/// <summary>
/// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment.
/// </summary>
PartiallySynced,
/// <summary>
/// Synced
/// </summary>
Synced,
/// <summary>
/// Disposed
/// </summary>
Diposed
}
/// <summary>
/// Status of the order book
/// </summary>
@@ -208,4 +235,18 @@
Cache
}
/// <summary>
/// Type of exchange
/// </summary>
public enum ExchangeType
{
/// <summary>
/// Centralized
/// </summary>
CEX,
/// <summary>
/// Decentralized
/// </summary>
DEX
}
}
@@ -0,0 +1,58 @@
using CryptoExchange.Net.Authentication;
using Microsoft.Extensions.DependencyInjection;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Library options
/// </summary>
/// <typeparam name="TRestOptions"></typeparam>
/// <typeparam name="TSocketOptions"></typeparam>
/// <typeparam name="TApiCredentials"></typeparam>
/// <typeparam name="TEnvironment"></typeparam>
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
where TRestOptions: RestExchangeOptions, new()
where TSocketOptions: SocketExchangeOptions, new()
where TApiCredentials: ApiCredentials
where TEnvironment: TradeEnvironment
{
/// <summary>
/// Rest client options
/// </summary>
public TRestOptions Rest { get; set; } = new TRestOptions();
/// <summary>
/// Socket client options
/// </summary>
public TSocketOptions Socket { get; set; } = new TSocketOptions();
/// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API.
/// </summary>
public TEnvironment? Environment { get; set; }
/// <summary>
/// The api credentials used for signing requests.
/// </summary>
public TApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// The DI service lifetime for the socket client
/// </summary>
public ServiceLifetime? SocketClientLifeTime { get; set; }
/// <summary>
/// Copy values from these options to the target options
/// </summary>
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
{
targetOptions.ApiCredentials = ApiCredentials;
targetOptions.Environment = Environment;
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
targetOptions.Rest = Rest.Set(targetOptions.Rest);
targetOptions.Socket = Socket.Set(targetOptions.Socket);
return targetOptions;
}
}
}
@@ -19,19 +19,15 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan? TimestampRecalculationInterval { get; set; }
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
public T Set<T>(T item) where T : RestApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
return item;
}
}
@@ -29,25 +29,21 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : RestExchangeOptions, new()
public T Set<T>(T item) where T : RestExchangeOptions, new()
{
return new T
{
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval,
ApiCredentials = ApiCredentials?.Copy(),
Proxy = Proxy,
RequestTimeout = RequestTimeout,
RateLimiterEnabled = RateLimiterEnabled,
RateLimitingBehaviour = RateLimitingBehaviour,
CachingEnabled = CachingEnabled,
CachingMaxAge = CachingMaxAge,
};
item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
item.ApiCredentials = ApiCredentials?.Copy();
item.Proxy = Proxy;
item.RequestTimeout = RequestTimeout;
item.RateLimiterEnabled = RateLimiterEnabled;
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge;
return item;
}
}
@@ -66,15 +62,13 @@ namespace CryptoExchange.Net.Objects.Options
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
{
var result = base.Copy<T>();
result.Environment = Environment;
return result;
base.Set(target);
target.Environment = Environment;
return target;
}
}
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
public int? MaxSocketConnections { get; set; }
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketApiOptions, new()
public T Set<T>(T item) where T : SocketApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
SocketNoDataTimeout = SocketNoDataTimeout,
MaxSocketConnections = MaxSocketConnections,
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.SocketNoDataTimeout = SocketNoDataTimeout;
item.MaxSocketConnections = MaxSocketConnections;
return item;
}
}
@@ -57,24 +57,22 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketExchangeOptions, new()
public T Set<T>(T item) where T : SocketExchangeOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
ReconnectPolicy = ReconnectPolicy,
DelayAfterConnect = DelayAfterConnect,
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
ReconnectInterval = ReconnectInterval,
SocketNoDataTimeout = SocketNoDataTimeout,
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
MaxSocketConnections = MaxSocketConnections,
Proxy = Proxy,
RequestTimeout = RequestTimeout,
RateLimitingBehaviour = RateLimitingBehaviour,
RateLimiterEnabled = RateLimiterEnabled,
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.ReconnectPolicy = ReconnectPolicy;
item.DelayAfterConnect = DelayAfterConnect;
item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
item.ReconnectInterval = ReconnectInterval;
item.SocketNoDataTimeout = SocketNoDataTimeout;
item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
item.MaxSocketConnections = MaxSocketConnections;
item.Proxy = Proxy;
item.RequestTimeout = RequestTimeout;
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimiterEnabled = RateLimiterEnabled;
return item;
}
}
@@ -93,15 +91,13 @@ namespace CryptoExchange.Net.Objects.Options
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
{
var result = base.Copy<T>();
result.Environment = Environment;
return result;
base.Set(target);
target.Environment = Environment;
return target;
}
}
@@ -0,0 +1,29 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Options
{
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<T> where T : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public T? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}
/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
}
@@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public int Compare(string x, string y)
public int Compare(string? x, string? y)
{
// Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0;
@@ -156,7 +156,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param>
public void AddSecondsString(string key, DateTime value)
{
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
}
/// <summary>
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.Objects
public void AddOptionalSecondsString(string key, DateTime? value)
{
if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
}
/// <summary>
@@ -187,7 +187,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value)
{
var stringVal = EnumConverter.GetString(value);
var stringVal = EnumConverter.GetString(value)!;
Add(key, int.Parse(stringVal)!);
}
@@ -58,12 +58,16 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public IRateLimitGuard? LimitGuard { get; set; }
/// <summary>
/// Whether this request should never be cached
/// </summary>
public bool PreventCaching { get; set; }
/// <summary>
/// Connection id
/// </summary>
public int? ConnectionId { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -58,9 +58,38 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null)
{
if (!_definitions.TryGetValue(method + path, out var def))
if (!_definitions.TryGetValue(identifier, out var def))
{
def = new RequestDefinition(path, method)
{
@@ -73,7 +102,7 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false
};
_definitions.TryAdd(method + path, def);
_definitions.TryAdd(identifier, def);
}
return def;
+2 -2
View File
@@ -82,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
{
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms", TimeSyncState.ApiName);
TimeSyncState.TimeOffset = TimeSpan.Zero;
}
else
{
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
Logger.Log(LogLevel.Information, "{TimeSyncState.ApiName} Time offset set to {Offset}ms", TimeSyncState.ApiName, Math.Round(offset.TotalMilliseconds));
TimeSyncState.TimeOffset = offset;
}
}
@@ -24,14 +24,14 @@
/// <summary>
/// Name of the environment
/// </summary>
public string EnvironmentName { get; init; }
public string Name { get; set; }
/// <summary>
/// </summary>
/// <param name="name"></param>
protected TradeEnvironment(string name)
{
EnvironmentName = name;
Name = name;
}
}
}
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System;
namespace CryptoExchange.Net.OrderBook
@@ -8,14 +9,14 @@ namespace CryptoExchange.Net.OrderBook
public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions
{
private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor;
private readonly Func<string, string, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
private readonly Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
/// <summary>
/// ctor
/// </summary>
/// <param name="symbolCtor"></param>
/// <param name="assetsCtor"></param>
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<string, string, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
{
_symbolCtor = symbolCtor;
_assetsCtor = assetsCtor;
@@ -25,6 +26,9 @@ namespace CryptoExchange.Net.OrderBook
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options);
/// <inheritdoc />
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(baseAsset, quoteAsset, options);
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset), options);
/// <inheritdoc />
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null) => _assetsCtor(symbol, options);
}
}
@@ -843,9 +843,9 @@ namespace CryptoExchange.Net.OrderBook
internal class DescComparer<T> : IComparer<T>
{
public int Compare(T x, T y)
public int Compare(T? x, T? y)
{
return Comparer<T>.Default.Compare(y, x);
return Comparer<T>.Default.Compare(y!, x!);
}
}
}
@@ -18,6 +18,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
/// <summary>
/// Apply guard per connection
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
/// <summary>
/// Apply guard per API key
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
@@ -106,7 +110,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
}
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
}
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary>
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>
/// Add a rate limit guard
/// </summary>
@@ -63,8 +68,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</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>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct);
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
}
}
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// No wait needed
/// </summary>
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current);
/// <summary>
/// Wait needed
@@ -4,10 +4,14 @@ using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit event
/// Rate limit triggered event
/// </summary>
public record RateLimitEvent
{
/// <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>
@@ -52,18 +56,9 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// ctor
/// </summary>
/// <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)
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
{
ItemId = itemId;
ApiLimit = apiLimit;
LimitDescription = limitDescription;
RequestDefinition = definition;
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.RateLimiting
/// <inheritdoc />
public event Action<RateLimitEvent>? RateLimitTriggered;
/// <inheritdoc />
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary>
/// ctor
@@ -67,6 +69,7 @@ namespace CryptoExchange.Net.RateLimiting
RequestDefinition definition,
string host,
string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour,
CancellationToken ct)
{
@@ -75,7 +78,7 @@ namespace CryptoExchange.Net.RateLimiting
_waitingCount++;
try
{
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -105,7 +108,7 @@ namespace CryptoExchange.Net.RateLimiting
else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
}
@@ -120,7 +123,7 @@ namespace CryptoExchange.Net.RateLimiting
else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
@@ -133,6 +136,8 @@ namespace CryptoExchange.Net.RateLimiting
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
if (result.IsApplied)
{
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
if (type == RateLimitItemType.Connection)
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
else
@@ -0,0 +1,50 @@
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 />
public Uri Uri => _request.RequestUri;
public Uri Uri => _request.RequestUri!;
/// <inheritdoc />
public int RequestId { get; }
+34 -18
View File
@@ -11,29 +11,13 @@ namespace CryptoExchange.Net.Requests
/// </summary>
public class RequestFactory : IRequestFactory
{
private HttpClient? _httpClient;
private HttpClient? _httpClient;
/// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{
if (client == null)
{
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
};
}
client = CreateClient(proxy, requestTimeout);
_httpClient = client;
}
@@ -46,5 +30,37 @@ namespace CryptoExchange.Net.Requests
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;
}
}
}
@@ -12,6 +12,10 @@
/// <summary>
/// Leverage is configured for the symbol
/// </summary>
PerSymbol
PerSymbol,
/// <summary>
/// Leverage is configured for the entire account
/// </summary>
PerAccount
}
}
@@ -18,6 +18,11 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
TradingMode[] SupportedTradingModes { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Client for requesting user trading fees
/// </summary>
public interface IFeeRestClient : ISharedClient
{
/// <summary>
/// Fee request options
/// </summary>
EndpointOptions<GetFeeRequest> GetFeeOptions { get; }
/// <summary>
/// Get trading fees for a symbol
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFee>> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default);
}
}
@@ -60,8 +60,8 @@ namespace CryptoExchange.Net.SharedApis
}
else
{
if (param.Names.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
}
}
@@ -113,13 +113,13 @@ namespace CryptoExchange.Net.SharedApis
{
if (!string.IsNullOrEmpty(param.Name))
{
if (typeof(T).GetProperty(param.Name).GetValue(request, null) == null)
if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
}
else
{
if (param.Names.All(x => typeof(T).GetProperty(param.Name).GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
}
}
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
{
TimeFilterSupported = timeFilterSupported;
}
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
{
TimeFilterSupported = timeFilterSupported;
}
@@ -8,7 +8,7 @@
/// <summary>
/// ctor
/// </summary>
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
{
}
}
@@ -20,10 +20,6 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public int? MaxTotalDataPoints { get; set; }
/// <summary>
/// Max number of data points which can be requested in a single request
/// </summary>
public int? MaxRequestDataPoints { get; set; }
/// <summary>
/// The max age of the data that can be requested
/// </summary>
public TimeSpan? MaxAge { get; set; }
@@ -31,14 +27,13 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public GetKlinesOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
{
SupportIntervals = new[]
{
SharedKlineInterval.FiveMinutes,
SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.OneHour,
SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.OneDay,
SharedKlineInterval.OneWeek,
SharedKlineInterval.OneMonth
@@ -48,7 +43,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public GetKlinesOptions(SharedPaginationSupport paginationType, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, needsAuthentication)
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
{
SupportIntervals = intervals;
}
@@ -69,8 +64,8 @@ namespace CryptoExchange.Net.SharedApis
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} klines are available");
if (MaxRequestDataPoints.HasValue && request.Limit > MaxRequestDataPoints.Value)
return new ArgumentError($"Only {MaxRequestDataPoints} klines can be retrieved per request");
if (request.Limit > MaxLimit)
return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request");
if (MaxTotalDataPoints.HasValue)
{
@@ -96,8 +91,6 @@ namespace CryptoExchange.Net.SharedApis
sb.AppendLine($"Max age of data: {MaxAge}");
if (MaxTotalDataPoints != null)
sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}");
if (MaxRequestDataPoints != null)
sb.AppendLine($"Max data points per request: {MaxRequestDataPoints}");
return sb.ToString();
}
}
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
public override string ToString(string exchange)
{
var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Supported limit values: [{(SupportedLimits == null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]");
sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]");
return sb.ToString();
}
}
@@ -8,7 +8,7 @@
/// <summary>
/// ctor
/// </summary>
public GetPositionHistoryOptions(SharedPaginationSupport paginationType) : base(paginationType, true)
public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
{
}
}
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
{
}
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
{
TimeFilterSupported = timeFilterSupported;
}
@@ -1,4 +1,5 @@
using System.Text;
using CryptoExchange.Net.Objects;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
@@ -13,12 +14,24 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public SharedPaginationSupport PaginationSupport { get; }
/// <summary>
/// Whether filtering based on start/end time is supported
/// </summary>
public bool TimePeriodFilterSupport { get; }
/// <summary>
/// Max amount of results that can be requested
/// </summary>
public int MaxLimit { get; set; }
/// <summary>
/// ctor
/// </summary>
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(needsAuthentication)
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
{
PaginationSupport = paginationType;
TimePeriodFilterSupport = timePeriodSupport;
MaxLimit = maxLimit;
}
/// <inheritdoc />
@@ -26,6 +39,8 @@ namespace CryptoExchange.Net.SharedApis
{
var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Pagination type: {PaginationSupport}");
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
sb.AppendLine($"Max limit: {MaxLimit}");
return sb.ToString();
}
}
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.SharedApis
{
if (Name != null)
return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}";
return $"[{ValueType.Name}] {string.Join(" / ", Names)}: {Description} | example: {ExampleValue}";
return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}";
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to retrieve trading fees
/// </summary>
public record GetFeeRequest : SharedSymbolRequest
{
/// <summary>
/// ctor
/// </summary>
/// <param name="symbol">Symbol to retrieve fees for</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public GetFeeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Trading fee info
/// </summary>
public record SharedFee
{
/// <summary>
/// Taker fee percentage
/// </summary>
public decimal TakerFee { get; set; }
/// <summary>
/// Maker fee percentage
/// </summary>
public decimal MakerFee { get; set; }
/// <summary>
/// ctor
/// </summary>
public SharedFee(decimal makerFee, decimal takerFee)
{
MakerFee = makerFee;
TakerFee = takerFee;
}
}
}
@@ -14,15 +14,15 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Last trade price
/// </summary>
public decimal LastPrice { get; set; }
public decimal? LastPrice { get; set; }
/// <summary>
/// High price in the last 24h
/// </summary>
public decimal HighPrice { get; set; }
public decimal? HighPrice { get; set; }
/// <summary>
/// Low price in the last 24h
/// </summary>
public decimal LowPrice { get; set; }
public decimal? LowPrice { get; set; }
/// <summary>
/// The volume in the last 24h
/// </summary>
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public SharedFuturesTicker(string symbol, decimal lastPrice, decimal highPrice, decimal lowPrice, decimal volume, decimal? changePercentage)
public SharedFuturesTicker(string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage)
{
Symbol = symbol;
LastPrice = lastPrice;
@@ -46,6 +46,10 @@
/// </summary>
public int? PriceDecimals { get; set; }
/// <summary>
/// The max amount of significant figures to use for price. For example with value of 5 these values are valid: 0.00001, 0.12300, 123.53, 12345, but this is not: 12345.1
/// </summary>
public int? PriceSignificantFigures { get; set; }
/// <summary>
/// Whether the symbol is currently available for trading
/// </summary>
public bool Trading { get; set; }
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
/// Trade time
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// Trade side. Buy means that the taker took an ask order of the order book, sell means the taker took a bid order of the order book.
/// </summary>
public SharedOrderSide? Side { get; set; }
/// <summary>
/// ctor
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Side of the trade
/// </summary>
public SharedOrderSide Side { get; set; }
public SharedOrderSide? Side { get; set; }
/// <summary>
/// Fee paid for the trade
/// </summary>
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide side, decimal quantity, decimal price, DateTime timestamp)
public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp)
{
Symbol = symbol;
OrderId = orderId;
@@ -1,12 +1,13 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// A symbol representation based on a base and quote asset
/// </summary>
public class SharedSymbol
public record SharedSymbol
{
/// <summary>
/// The base asset of the symbol
@@ -155,6 +155,12 @@ namespace CryptoExchange.Net.Sockets
_baseAddress = $"{Uri.Scheme}://{Uri.Host}";
}
/// <inheritdoc />
public void UpdateProxy(ApiProxy? proxy)
{
Parameters.Proxy = proxy;
}
/// <inheritdoc />
public virtual async Task<CallResult> ConnectAsync()
{
@@ -189,9 +195,12 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER
#if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true;
#endif
#endif
#if NET9_0_OR_GREATER
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
#endif
}
catch (PlatformNotSupportedException)
{
@@ -209,7 +218,7 @@ namespace CryptoExchange.Net.Sockets
{
if (Parameters.RateLimiter != null)
{
var definition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult)
return new CallResult(new ClientRateLimitError("Connection limit reached"));
@@ -229,13 +238,13 @@ namespace CryptoExchange.Net.Sockets
if (e is WebSocketException we)
{
#if (NET6_0_OR_GREATER)
#if (NET6_0_OR_GREATER)
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
{
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message));
}
#else
#else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead
if (we.Message.Contains("429"))
@@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message));
}
#endif
#endif
}
return new CallResult(new CantConnectError());
@@ -435,8 +444,8 @@ namespace CryptoExchange.Net.Sockets
{
// Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5))
break; // Wait for max 5 seconds, then just abort the connection
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
break; // Wait for max 1 second, then just abort the connection
}
}
}
@@ -475,7 +484,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
private async Task SendLoopAsync()
{
var requestDefinition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
try
{
while (true)
@@ -578,15 +587,27 @@ namespace CryptoExchange.Net.Sockets
lock (_receivedMessagesLock)
_receivedMessages.Add(new ReceiveItem(DateTime.UtcNow, receiveResult.Count));
}
catch (OperationCanceledException)
catch (OperationCanceledException ex)
{
if (ex.InnerException?.InnerException?.Message.Equals("The WebSocket didn't recieve a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.") == true)
{
// Spefic case that the websocket connection got closed because of a ping frame timeout
// Unfortunately doesn't seem to be a nicer way to catch
_logger.SocketPingTimeout(Id);
}
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
// canceled
break;
}
catch (Exception wse)
{
// Connection closed unexpectedly
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
if (!_ctsSource.Token.IsCancellationRequested && !_stopRequested)
// Connection closed unexpectedly
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@@ -598,14 +619,14 @@ namespace CryptoExchange.Net.Sockets
if (_socket.State == WebSocketState.CloseReceived)
{
// Close received means it server initiated, we should send a confirmation and close the socket
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
}
else
{
// Means the socket is now closed and we were the one initiating it
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
}
break;
@@ -620,7 +641,7 @@ namespace CryptoExchange.Net.Sockets
// Write the data to a memory stream to be reassembled later
if (multipartStream == null)
multipartStream = new MemoryStream();
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count);
multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
}
else
{
@@ -634,7 +655,7 @@ namespace CryptoExchange.Net.Sockets
{
// Received the end of a multipart message, write to memory stream for reassembling
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count);
multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
}
break;
@@ -14,4 +14,19 @@
/// </summary>
public bool Authenticated { get; set; }
}
/// <summary>
/// Dedicated connection state
/// </summary>
public class DedicatedConnectionState
{
/// <summary>
/// Whether the connection is a dedicated request connection
/// </summary>
public bool IsDedicatedRequestConnection { get; set; }
/// <summary>
/// Whether the dedication request connection should be authenticated
/// </summary>
public bool Authenticated { get; set; }
}
}
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Callback after query
/// </summary>
public Action<CallResult>? Callback { get; set; }
public Action<SocketConnection, CallResult>? Callback { get; set; }
}
}
+17 -1
View File
@@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool Completed { get; set; }
/// <summary>
/// Timeout for the request
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
/// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response
@@ -177,6 +182,10 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc />
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
{
var typedMessage = message.As((TServerResponse)message.Data);
if (!ValidateMessage(typedMessage))
return new CallResult(null);
CurrentResponses++;
if (CurrentResponses == RequiredResponses)
{
@@ -186,7 +195,7 @@ namespace CryptoExchange.Net.Sockets
if (Result?.Success != false)
// If an error result is already set don't override that
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
Result = HandleMessage(connection, typedMessage);
if (CurrentResponses == RequiredResponses)
{
@@ -198,6 +207,13 @@ namespace CryptoExchange.Net.Sockets
return Result;
}
/// <summary>
/// Validate if a message is actually processable by this query
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public virtual bool ValidateMessage(DataEvent<TServerResponse> message) => true;
/// <summary>
/// Handle the query response
/// </summary>
+51 -13
View File
@@ -11,6 +11,8 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions;
using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets
{
@@ -186,9 +188,21 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Whether this connection should be kept alive even when there is no subscription
/// Info on whether this connection is a dedicated request connection
/// </summary>
public bool DedicatedRequestConnection { get; internal set; }
public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState();
/// <summary>
/// Current subscription topics on this connection
/// </summary>
public IEnumerable<string> Topics
{
get
{
lock (_listenersLock)
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToList()!;
}
}
private bool _pausedActivity;
private readonly object _listenersLock;
@@ -268,7 +282,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
{
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
subscription.Confirmed = false;
subscription.Reset();
foreach (var query in _listeners.OfType<Query>().ToList())
{
@@ -293,7 +307,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
{
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
subscription.Confirmed = false;
subscription.Reset();
foreach (var query in _listeners.OfType<Query>().ToList())
{
@@ -384,7 +398,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{
Query query;
Query? query;
lock (_listenersLock)
{
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -413,7 +427,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="requestId">Id of the request sent</param>
protected virtual Task HandleRequestSentAsync(int requestId)
{
Query query;
Query? query;
lock (_listenersLock)
{
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -425,7 +439,7 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask;
}
query.IsSend(ApiClient.ClientOptions.RequestTimeout);
query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
return Task.CompletedTask;
}
@@ -571,6 +585,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
/// <summary>
/// Update the proxy setting and reconnect
/// </summary>
/// <param name="proxy">New proxy setting</param>
public async Task UpdateProxy(ApiProxy? proxy)
{
_socket.UpdateProxy(proxy);
await TriggerReconnectAsync().ConfigureAwait(false);
}
/// <summary>
/// Close the connection
/// </summary>
@@ -603,6 +627,10 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public async Task CloseAsync(Subscription subscription)
{
// If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues
while (subscription.IsResubscribing)
await Task.Delay(50).ConfigureAwait(false);
subscription.Closed = true;
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
@@ -618,7 +646,7 @@ namespace CryptoExchange.Net.Sockets
bool shouldCloseConnection;
lock (_listenersLock)
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
if (!anyDuplicateSubscription)
{
@@ -841,7 +869,7 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen)
return new CallResult(new WebError("Socket not connected"));
if (!DedicatedRequestConnection)
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
{
bool anySubscriptions;
lock (_listenersLock)
@@ -859,7 +887,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
{
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|| (DedicatedRequestConnection && ApiClient.AuthenticationProvider != null);
|| (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated);
}
if (anyAuthenticated)
@@ -886,7 +914,7 @@ namespace CryptoExchange.Net.Sockets
List<Subscription> subList;
lock (_listenersLock)
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
if (subList.Count == 0)
break;
@@ -895,20 +923,30 @@ namespace CryptoExchange.Net.Sockets
foreach (var subscription in subList)
{
subscription.ConnectionInvocations = 0;
if (subscription.Closed)
// Can be closed during resubscribing
continue;
subscription.IsResubscribing = true;
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result)
{
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
subscription.IsResubscribing = false;
return result;
}
var subQuery = subscription.GetSubQuery(this);
if (subQuery == null)
{
subscription.IsResubscribing = false;
continue;
}
var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{
subscription.IsResubscribing = false;
subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set();
if (r.Result.Success)
@@ -962,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="interval">How often</param>
/// <param name="queryDelegate">Method returning the query to send</param>
/// <param name="callback">The callback for processing the response</param>
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{
if (queryDelegate == null)
throw new ArgumentNullException(nameof(queryDelegate));
@@ -994,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets
try
{
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
callback?.Invoke(result);
callback?.Invoke(this, result);
}
catch (Exception ex)
{
@@ -44,6 +44,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool Closed { get; set; }
/// <summary>
/// Is the subscription currently resubscribing
/// </summary>
public bool IsResubscribing { get; set; }
/// <summary>
/// Logger
/// </summary>
@@ -76,6 +81,11 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// Subscription topic
/// </summary>
public string? Topic { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -130,6 +140,20 @@ namespace CryptoExchange.Net.Sockets
return Task.FromResult(DoHandleMessage(connection, message));
}
/// <summary>
/// Reset the subscription
/// </summary>
public void Reset()
{
Confirmed = false;
DoHandleReset();
}
/// <summary>
/// Connection has been reset, do any logic for resetting the subscription
/// </summary>
public virtual void DoHandleReset() { }
/// <summary>
/// Handle the update message
/// </summary>
@@ -378,7 +378,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()))
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
}
}
@@ -105,7 +105,7 @@ namespace CryptoExchange.Net.Testing.Comparers
int i = 0;
foreach (var item in jObj.Children())
{
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null)
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
i++;
@@ -211,7 +211,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object)
{
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties);
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties);
}
else
{
@@ -224,6 +224,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
&& propertyValue.GetType() != typeof(string))
{
if (propValue.Type != JTokenType.Array)
return;
var jObjs = (JArray)propValue;
var list = (IEnumerable)propertyValue;
var enumerator = list.GetEnumerator();
@@ -261,9 +264,9 @@ namespace CryptoExchange.Net.Testing.Comparers
int i = 0;
foreach (var item in jtoken.Children())
{
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null)
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
i++;
}
@@ -347,7 +350,7 @@ namespace CryptoExchange.Net.Testing.Comparers
int i = 0;
foreach (var item in jObjs.Children())
{
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p;
if (arrayProp != null)
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
i++;
@@ -372,7 +375,8 @@ namespace CryptoExchange.Net.Testing.Comparers
}
else if (objectValue is DateTime time)
{
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
var jsonStr = jsonValue.Value<string>()!;
if (!string.IsNullOrEmpty(jsonStr) && time != DateTimeConverter.ParseFromString(jsonStr))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
}
else if (objectValue is bool bl)

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