1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-13 09:23:04 +00:00

Compare commits

...

31 Commits

Author SHA1 Message Date
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
95 changed files with 1808 additions and 460 deletions
@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"></PackageReference> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference> <PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
var result = ExchangeHelpers.Normalize(input); var result = ExchangeHelpers.Normalize(input);
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture)); Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
} }
[Test]
[TestCase("123", "BKR", 32, true, "BKRJK123")]
[TestCase("123", "BKR", 32, false, "123")]
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
[TestCase(null, "BKR", 32, true, null)]
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
{
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
if (expected != null)
Assert.That(result, Is.EqualTo(expected));
}
} }
} }
+21 -5
View File
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider1.GetSecret() == "222"); Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123"); Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
[Test] [Test]
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider2.GetKey() == "123"); Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123"); Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
} }
@@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
Environment = new TestEnvironment("test", "https://test.com") Environment = new TestEnvironment("test", "https://test.com")
}; };
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary> /// <summary>
/// The default receive window for requests /// The default receive window for requests
/// </summary> /// </summary>
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
public RestApiOptions Api2Options { get; set; } = new RestApiOptions(); public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy() internal TestClientOptions Set(TestClientOptions targetOptions)
{ {
var options = Copy<TestClientOptions>(); targetOptions = base.Set<TestClientOptions>(targetOptions);
options.Api1Options = Api1Options.Copy<RestApiOptions>(); targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
options.Api2Options = Api2Options.Copy<RestApiOptions>(); targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return options; return targetOptions;
} }
} }
} }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test") public TestBaseClient(): base(null, "Test")
{ {
var options = TestClientOptions.Default.Copy(); var options = new TestClientOptions();
Initialize(options); Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions())); SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
} }
@@ -16,6 +16,7 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -24,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; } public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; } public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> 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") Api1 = new TestRestApi1Client(options.Value);
{ Api2 = new TestRestApi2Client(options.Value);
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
} }
public void SetResponse(string responseData, out IRequest requestObj) public void SetResponse(string responseData, out IRequest requestObj)
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
using Moq; using Moq;
using CryptoExchange.Net.Testing.Implementations; using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -22,25 +23,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
public TestSubSocketClient SubClient { get; } public TestSubSocketClient SubClient { get; }
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
/// <summary> /// <summary>
/// Create a new instance of KucoinSocketClient /// Create a new instance of KucoinSocketClient
/// </summary> /// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param> /// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> 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>(); Initialize(options.Value);
optionsFunc(options);
Initialize(options);
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions)); SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object; SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com")); Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
} }
@@ -70,7 +66,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
Environment = new TestEnvironment("Live", "https://test.test") Environment = new TestEnvironment("Live", "https://test.test")
}; };
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions(); public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
} }
public class TestSubSocketClient : SocketApiClient public class TestSubSocketClient : SocketApiClient
@@ -1,4 +1,4 @@
#if !NETSTANDARD2_1 #if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis namespace System.Diagnostics.CodeAnalysis
{ {
using System; using System;
@@ -13,39 +13,30 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// The api key / label to authenticate requests /// The api key / label to authenticate requests
/// </summary> /// </summary>
public string Key { get; } public string Key { get; set; }
/// <summary> /// <summary>
/// The api secret or private key to authenticate requests /// The api secret or private key to authenticate requests
/// </summary> /// </summary>
public string Secret { get; } public string Secret { get; set; }
/// <summary> /// <summary>
/// Type of the credentials /// Type of the credentials
/// </summary> /// </summary>
public ApiCredentialsType CredentialType { get; } public ApiCredentialsType CredentialType { get; set; }
/// <summary> /// <summary>
/// Create Api credentials providing an api key and secret for authentication /// Create Api credentials providing an api key and secret for authentication
/// </summary> /// </summary>
/// <param name="key">The api key / label used for identification</param> /// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</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) /// <param name="credentialType">The type of credentials</param>
{ public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / 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)
{ {
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty"); throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType; CredentialType = credentialType;
Key = key; Key = key;
Secret = secret; Secret = secret;
} }
@@ -65,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="inputStream">The stream containing the json data</param> /// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param> /// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param> /// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
public 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(); var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result) if (!accessor.Read(inputStream, false).Result)
@@ -75,11 +66,9 @@ namespace CryptoExchange.Net.Authentication
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret")); var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null) if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key;
Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin); inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(key, secret);
} }
} }
} }
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Get the API key of the current credentials /// Get the API key of the current credentials
/// </summary> /// </summary>
public string ApiKey => _credentials.Key; public string ApiKey => _credentials.Key!;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="credentials"></param> /// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials) protected AuthenticationProvider(ApiCredentials credentials)
{ {
if (credentials.Secret == null) if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed"); throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials; _credentials = credentials;
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.Authentication
var rsa = RSA.Create(); var rsa = RSA.Create();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem) if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
{ {
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key // Read from pem private key
var key = _credentials.Secret! var key = _credentials.Secret!
.Replace("\n", "") .Replace("\n", "")
+1 -1
View File
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Caching
/// <returns>Cached value if it was in cache</returns> /// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge) public object? Get(string key, TimeSpan maxAge)
{ {
_cache.TryGetValue(key, out CacheItem value); _cache.TryGetValue(key, out CacheItem? value);
if (value == null) if (value == null)
return null; return null;
@@ -93,6 +93,17 @@ namespace CryptoExchange.Net.Clients
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
} }
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
if (options.ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+12 -2
View File
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Version of the CryptoExchange.Net base library /// Version of the CryptoExchange.Net base library
/// </summary> /// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version; public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary> /// <summary>
/// Version of the client implementation /// Version of the client implementation
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Clients
lock(_versionLock) lock(_versionLock)
{ {
if (_exchangeVersion == null) if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version; _exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion; return _exchangeVersion;
} }
@@ -109,6 +109,16 @@ namespace CryptoExchange.Net.Clients
return apiClient; return apiClient;
} }
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+9 -1
View File
@@ -807,7 +807,7 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InUri) if (parameterPosition == HttpMethodParameterPosition.InUri)
{ {
foreach (var parameter in parameters) foreach (var parameter in parameters)
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()); uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString()!);
} }
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
@@ -961,6 +961,14 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns> /// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
+22 -2
View File
@@ -158,7 +158,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -422,9 +422,10 @@ namespace CryptoExchange.Net.Clients
result.Error!.Message = "Authentication failed: " + result.Error.Message; result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!; return new CallResult(result.Error)!;
} }
_logger.Authenticated(socket.SocketId);
} }
_logger.Authenticated(socket.SocketId);
socket.Authenticated = true; socket.Authenticated = true;
return new CallResult(null); return new CallResult(null);
} }
@@ -710,6 +711,25 @@ namespace CryptoExchange.Net.Clients
return new CallResult(null); return new CallResult(null);
} }
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null)
|| !socketConnections.Any())
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
var result = Activator.CreateInstance(objectType); var result = Activator.CreateInstance(objectType);
var arr = JArray.Load(reader); var arr = JArray.Load(reader);
return ParseObject(arr, result, objectType); return ParseObject(arr, result!, objectType);
} }
private static object ParseObject(JArray arr, object result, Type objectType) private static object ParseObject(JArray arr, object result, Type objectType)
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters.JsonNet
var count = 0; var count = 0;
if (innerArray.Count == 0) if (innerArray.Count == 0)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else if (innerArray[0].Type == JTokenType.Array) else if (innerArray[0].Type == JTokenType.Array)
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
foreach (var obj in innerArray) foreach (var obj in innerArray)
{ {
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!); arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
count++; count++;
} }
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
else else
{ {
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 }); var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
var innerObj = Activator.CreateInstance(objType!); var innerObj = Activator.CreateInstance(objType!);
arrayResult[0] = ParseObject(innerArray, innerObj, objType!); arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
property.SetValue(result, arrayResult); property.SetValue(result, arrayResult);
} }
continue; continue;
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
object? value; object? value;
if (converterAttribute != null) if (converterAttribute != null)
{ {
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}}); value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
} }
else if (conversionAttribute != null) else if (conversionAttribute != null)
{ {
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
else if ((property.PropertyType == typeof(decimal) else if ((property.PropertyType == typeof(decimal)
|| property.PropertyType == typeof(decimal?)) || property.PropertyType == typeof(decimal?))
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0)) && (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
{ {
var v = value.ToString(); var v = value.ToString();
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec)) if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
last = arrayProp.Index; last = arrayProp.Index;
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop); var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
if (converterAttribute != null) if (converterAttribute != null)
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType))); writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
else if (!IsSimple(prop.PropertyType)) else if (!IsSimple(prop.PropertyType))
serializer.Serialize(writer, prop.GetValue(value)); serializer.Serialize(writer, prop.GetValue(value));
else else
@@ -187,9 +187,9 @@ namespace CryptoExchange.Net.Converters.JsonNet
} }
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute => private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))); (T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
private static T? GetCustomAttribute<T>(Type type) where T : Attribute => private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))); (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
} }
} }
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
{ {
@@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
{ {
try try
{ {
var value = reader.Value!.ToString(); var value = reader.Value!.ToString()!;
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException) catch (OverflowException)
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
/// </returns> /// </returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var value = reader.Value?.ToString().ToLower().Trim(); var value = reader.Value?.ToString()!.ToLower().Trim();
if (value == null || value == "") if (value == null || value == "")
{ {
if (Nullable.GetUnderlyingType(objectType) != null) if (Nullable.GetUnderlyingType(objectType) != null)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
using var jsonTextReader = new JsonTextReader(reader); using var jsonTextReader = new JsonTextReader(reader);
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class ArrayPropertyInfo private class ArrayPropertyInfo
@@ -79,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonSerializerOptions? typeOptions = null; JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverterType != null) if (prop.JsonConverterType != null)
{ {
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
typeOptions = new JsonSerializerOptions(); typeOptions = new JsonSerializerOptions();
typeOptions.Converters.Clear(); typeOptions.Converters.Clear();
typeOptions.Converters.Add(converter); typeOptions.Converters.Add(converter);
@@ -90,7 +90,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (prop.PropertyInfo.PropertyType == typeof(string)) if (prop.PropertyInfo.PropertyType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
} }
else else
{ {
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.Null) if (reader.TokenType == JsonTokenType.Null)
return default; return default;
var result = Activator.CreateInstance(typeToConvert); var result = Activator.CreateInstance(typeToConvert)!;
return (T)ParseObject(ref reader, result, typeToConvert, options); return (T)ParseObject(ref reader, result, typeToConvert, options);
} }
@@ -177,7 +177,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
{ {
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType); var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
newOptions = new JsonSerializerOptions newOptions = new JsonSerializerOptions
{ {
NumberHandling = SerializerOptions.WithConverters.NumberHandling, NumberHandling = SerializerOptions.WithConverters.NumberHandling,
@@ -209,7 +209,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value == null ? null : value); attribute.PropertyInfo.SetValue(result, value);
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture); return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch(OverflowException) catch(OverflowException)
{ {
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class BoolConverterInner<T> : JsonConverter<T> private class BoolConverterInner<T> : JsonConverter<T>
@@ -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) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType); return (JsonConverter)Activator.CreateInstance(converterType)!;
} }
private class DateTimeConverterInner<T> : JsonConverter<T> private class DateTimeConverterInner<T> : JsonConverter<T>
@@ -74,7 +74,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{ {
if (value == null) if (value == null)
{
writer.WriteNullValue(); writer.WriteNullValue();
}
else else
{ {
var dtValue = (DateTime)(object)value; var dtValue = (DateTime)(object)value;
@@ -19,9 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String) if (reader.TokenType == JsonTokenType.String)
{ {
var value = reader.GetString(); var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value)) if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
return null; return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try try
{ {
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); 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);
}
}
@@ -0,0 +1,31 @@
using System;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Attribute for allowing specifying a JsonConverter with constructor parameters
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConverterCtorAttribute : JsonConverterAttribute
{
private readonly object[] _parameters;
private readonly Type _type;
/// <summary>
/// ctor
/// </summary>
public JsonConverterCtorAttribute(Type type, params object[] parameters)
{
_type = type;
_parameters = parameters;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert)
{
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Replace a value on a string property
/// </summary>
public class ReplaceConverter : JsonConverter<string>
{
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
/// <summary>
/// ctor
/// </summary>
public ReplaceConverter(params string[] replaceSets)
{
_replacementSets = replaceSets.Select(x =>
{
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
if (split.Length != 2)
throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]);
}).ToArray();
}
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
foreach (var set in _replacementSets)
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
return value;
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
}
@@ -21,6 +21,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
protected JsonDocument? _document; protected JsonDocument? _document;
private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
private JsonSerializerOptions? _customSerializerOptions;
/// <inheritdoc /> /// <inheritdoc />
public bool IsJson { get; set; } public bool IsJson { get; set; }
@@ -31,6 +32,21 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public object? Underlying => throw new NotImplementedException(); public object? Underlying => throw new NotImplementedException();
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
{
_customSerializerOptions = options;
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult<object> Deserialize(Type type, MessagePath? path = null) public CallResult<object> Deserialize(Type type, MessagePath? path = null)
{ {
@@ -42,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize(type, _serializerOptions); var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
return new CallResult<object>(result!); return new CallResult<object>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -65,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
var result = _document.Deserialize<T>(_serializerOptions); var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
return new CallResult<T>(result!); return new CallResult<T>(result!);
} }
catch (JsonException ex) catch (JsonException ex)
@@ -129,7 +145,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
try try
{ {
return value.Value.Deserialize<T>(_serializerOptions); return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
} }
catch { } catch { }
return default; return default;
@@ -223,6 +239,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override bool OriginalDataAvailable => _stream?.CanSeek == true; public override bool OriginalDataAvailable => _stream?.CanSeek == true;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(): base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<CallResult> Read(Stream stream, bool bufferStream) public async Task<CallResult> Read(Stream stream, bool bufferStream)
{ {
@@ -286,6 +316,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private ReadOnlyMemory<byte> _bytes; private ReadOnlyMemory<byte> _bytes;
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor() : base()
{
}
/// <summary>
/// ctor
/// </summary>
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
{
}
/// <inheritdoc /> /// <inheritdoc />
public CallResult Read(ReadOnlyMemory<byte> data) public CallResult Read(ReadOnlyMemory<byte> data)
{ {
+11 -10
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>8.2.0</PackageVersion> <PackageVersion>8.5.0</PackageVersion>
<AssemblyVersion>8.2.0</AssemblyVersion> <AssemblyVersion>8.5.0</AssemblyVersion>
<FileVersion>8.2.0</FileVersion> <FileVersion>8.5.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -20,7 +20,7 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion> <LangVersion>12.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -48,16 +48,17 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0"> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" /> <PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+1 -1
View File
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net
{ {
var randomChars = new char[length]; var randomChars = new char[length];
#if NETSTANDARD2_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)]; randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
#else #else
+3 -3
View File
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&"; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
} }
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
@@ -111,7 +111,7 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
} }
} }
return formData.ToString(); return formData.ToString()!;
} }
/// <summary> /// <summary>
@@ -366,7 +366,7 @@ namespace CryptoExchange.Net
{ {
using var decompressedStream = new MemoryStream(); using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray()); : new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream); deflateStream.CopyTo(decompressedStream);
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
@@ -31,5 +32,12 @@ namespace CryptoExchange.Net.Interfaces
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="credentials"></param> /// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials; void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api crentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
} }
} }
@@ -24,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="requestTimeout">Request timeout to use</param>
/// <param name="httpClient">Optional shared http client instance</param> /// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param> /// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null); void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
} }
} }
@@ -93,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task CloseAsync(); Task CloseAsync();
/// <summary>
/// Update proxy setting
/// </summary>
void UpdateProxy(ApiProxy? proxy);
} }
} }
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net
{
/// <summary>
/// Helpers for client libraries
/// </summary>
public static class LibraryHelpers
{
/// <summary>
/// Client order id 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;
}
}
}
+5 -14
View File
@@ -8,30 +8,21 @@
/// <summary> /// <summary>
/// The host address of the proxy /// The host address of the proxy
/// </summary> /// </summary>
public string Host { get; } public string Host { get; set; }
/// <summary> /// <summary>
/// The port of the proxy /// The port of the proxy
/// </summary> /// </summary>
public int Port { get; } public int Port { get; set; }
/// <summary> /// <summary>
/// The login of the proxy /// The login of the proxy
/// </summary> /// </summary>
public string? Login { get; } public string? Login { get; set; }
/// <summary> /// <summary>
/// The password of the proxy /// The password of the proxy
/// </summary> /// </summary>
public string? Password { get; } public string? Password { get; set; }
/// <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)
{
}
/// <summary> /// <summary>
/// Create new settings for a proxy /// Create new settings for a proxy
@@ -40,7 +31,7 @@
/// <param name="port">The proxy port</param> /// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param> /// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param> /// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, string? password) public ApiProxy(string host, int port, string? login = null, string? password = null)
{ {
Host = host; Host = host;
Port = port; Port = port;
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(byte[] x, byte[] y) public int Compare(byte[]? x, byte[]? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
@@ -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; } public TimeSpan? TimestampRecalculationInterval { get; set; }
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public T Set<T>(T item) where T : RestApiOptions, new()
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
{ {
return new T item.ApiCredentials = ApiCredentials?.Copy();
{ item.OutputOriginalData = OutputOriginalData;
ApiCredentials = ApiCredentials?.Copy(), item.AutoTimestamp = AutoTimestamp;
OutputOriginalData = OutputOriginalData, item.TimestampRecalculationInterval = TimestampRecalculationInterval;
AutoTimestamp = AutoTimestamp, return item;
TimestampRecalculationInterval = TimestampRecalculationInterval
};
} }
} }
@@ -29,25 +29,21 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public T Set<T>(T item) where T : RestExchangeOptions, new()
/// <returns></returns>
public T Copy<T>() where T : RestExchangeOptions, new()
{ {
return new T item.OutputOriginalData = OutputOriginalData;
{ item.AutoTimestamp = AutoTimestamp;
OutputOriginalData = OutputOriginalData, item.TimestampRecalculationInterval = TimestampRecalculationInterval;
AutoTimestamp = AutoTimestamp, item.ApiCredentials = ApiCredentials?.Copy();
TimestampRecalculationInterval = TimestampRecalculationInterval, item.Proxy = Proxy;
ApiCredentials = ApiCredentials?.Copy(), item.RequestTimeout = RequestTimeout;
Proxy = Proxy, item.RateLimiterEnabled = RateLimiterEnabled;
RequestTimeout = RequestTimeout, item.RateLimitingBehaviour = RateLimitingBehaviour;
RateLimiterEnabled = RateLimiterEnabled, item.CachingEnabled = CachingEnabled;
RateLimitingBehaviour = RateLimitingBehaviour, item.CachingMaxAge = CachingMaxAge;
CachingEnabled = CachingEnabled, return item;
CachingMaxAge = CachingMaxAge,
};
} }
} }
@@ -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. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
/// <returns></returns>
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
{ {
var result = base.Copy<T>(); base.Set(target);
result.Environment = Environment; target.Environment = Environment;
return result; return target;
} }
} }
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
public int? MaxSocketConnections { get; set; } public int? MaxSocketConnections { get; set; }
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public T Set<T>(T item) where T : SocketApiOptions, new()
/// <returns></returns>
public T Copy<T>() where T : SocketApiOptions, new()
{ {
return new T item.ApiCredentials = ApiCredentials?.Copy();
{ item.OutputOriginalData = OutputOriginalData;
ApiCredentials = ApiCredentials?.Copy(), item.SocketNoDataTimeout = SocketNoDataTimeout;
OutputOriginalData = OutputOriginalData, item.MaxSocketConnections = MaxSocketConnections;
SocketNoDataTimeout = SocketNoDataTimeout, return item;
MaxSocketConnections = MaxSocketConnections,
};
} }
} }
@@ -57,24 +57,22 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <returns></returns> /// <returns></returns>
public T Copy<T>() where T : SocketExchangeOptions, new() public T Set<T>(T item) where T : SocketExchangeOptions, new()
{ {
return new T item.ApiCredentials = ApiCredentials?.Copy();
{ item.OutputOriginalData = OutputOriginalData;
ApiCredentials = ApiCredentials?.Copy(), item.ReconnectPolicy = ReconnectPolicy;
OutputOriginalData = OutputOriginalData, item.DelayAfterConnect = DelayAfterConnect;
ReconnectPolicy = ReconnectPolicy, item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
DelayAfterConnect = DelayAfterConnect, item.ReconnectInterval = ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket, item.SocketNoDataTimeout = SocketNoDataTimeout;
ReconnectInterval = ReconnectInterval, item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
SocketNoDataTimeout = SocketNoDataTimeout, item.MaxSocketConnections = MaxSocketConnections;
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget, item.Proxy = Proxy;
MaxSocketConnections = MaxSocketConnections, item.RequestTimeout = RequestTimeout;
Proxy = Proxy, item.RateLimitingBehaviour = RateLimitingBehaviour;
RequestTimeout = RequestTimeout, item.RateLimiterEnabled = RateLimiterEnabled;
RateLimitingBehaviour = RateLimitingBehaviour, return item;
RateLimiterEnabled = RateLimiterEnabled,
};
} }
} }
@@ -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. #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary> /// <summary>
/// Create a copy of this options /// Set the values of this options on the target options
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
/// <returns></returns>
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
{ {
var result = base.Copy<T>(); base.Set(target);
result.Environment = Environment; target.Environment = Environment;
return result; 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="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(string x, string y) public int Compare(string? x, string? y)
{ {
// Shortcuts: If both are null, they are the same. // Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0; if (x == null && y == null) return 0;
@@ -156,7 +156,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddSecondsString(string key, DateTime value) public void AddSecondsString(string key, DateTime value)
{ {
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.Objects
public void AddOptionalSecondsString(string key, DateTime? value) public void AddOptionalSecondsString(string key, DateTime? value)
{ {
if (value != null) if (value != null)
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()); Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
} }
/// <summary> /// <summary>
@@ -187,7 +187,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="value"></param> /// <param name="value"></param>
public void AddEnumAsInt<T>(string key, T value) public void AddEnumAsInt<T>(string key, T value)
{ {
var stringVal = EnumConverter.GetString(value); var stringVal = EnumConverter.GetString(value)!;
Add(key, int.Parse(stringVal)!); Add(key, int.Parse(stringVal)!);
} }
@@ -24,14 +24,14 @@
/// <summary> /// <summary>
/// Name of the environment /// Name of the environment
/// </summary> /// </summary>
public string EnvironmentName { get; init; } public string Name { get; set; }
/// <summary> /// <summary>
/// </summary> /// </summary>
/// <param name="name"></param> /// <param name="name"></param>
protected TradeEnvironment(string name) protected TradeEnvironment(string name)
{ {
EnvironmentName = name; Name = name;
} }
} }
} }
@@ -843,9 +843,9 @@ namespace CryptoExchange.Net.OrderBook
internal class DescComparer<T> : IComparer<T> internal class DescComparer<T> : IComparer<T>
{ {
public int Compare(T x, T y) public int Compare(T? x, T? y)
{ {
return Comparer<T>.Default.Compare(y, x); return Comparer<T>.Default.Compare(y!, x!);
} }
} }
} }
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary> /// <summary>
/// Apply guard per connection /// Apply guard per connection
/// </summary> /// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()); public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
/// <summary> /// <summary>
/// Apply guard per API key /// Apply guard per API key
/// </summary> /// </summary>
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded; return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
return LimitCheck.Needed(delay, 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); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded; return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
return LimitCheck.Needed(delay, _limit, _period, tracker.Current); return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
} }
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary> /// </summary>
event Action<RateLimitEvent> RateLimitTriggered; event Action<RateLimitEvent> RateLimitTriggered;
/// <summary>
/// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying.
/// </summary>
event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary> /// <summary>
/// Add a rate limit guard /// Add a rate limit guard
/// </summary> /// </summary>
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary> /// <summary>
/// No wait needed /// No wait needed
/// </summary> /// </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> /// <summary>
/// Wait needed /// Wait needed
@@ -4,10 +4,14 @@ using System;
namespace CryptoExchange.Net.RateLimiting namespace CryptoExchange.Net.RateLimiting
{ {
/// <summary> /// <summary>
/// Rate limit event /// Rate limit triggered event
/// </summary> /// </summary>
public record RateLimitEvent public record RateLimitEvent
{ {
/// <summary>
/// Id of the item the limit was checked for
/// </summary>
public int ItemId { get; set; }
/// <summary> /// <summary>
/// Name of the API limit that is reached /// Name of the API limit that is reached
/// </summary> /// </summary>
@@ -52,18 +56,9 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="apiLimit"></param> public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
/// <param name="limitDescription"></param>
/// <param name="definition"></param>
/// <param name="host"></param>
/// <param name="current"></param>
/// <param name="requestWeight"></param>
/// <param name="limit"></param>
/// <param name="timePeriod"></param>
/// <param name="delayTime"></param>
/// <param name="behaviour"></param>
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
{ {
ItemId = itemId;
ApiLimit = apiLimit; ApiLimit = apiLimit;
LimitDescription = limitDescription; LimitDescription = limitDescription;
RequestDefinition = definition; RequestDefinition = definition;
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.RateLimiting
/// <inheritdoc /> /// <inheritdoc />
public event Action<RateLimitEvent>? RateLimitTriggered; public event Action<RateLimitEvent>? RateLimitTriggered;
/// <inheritdoc />
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -105,7 +107,7 @@ namespace CryptoExchange.Net.RateLimiting
else else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_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}")); return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
} }
@@ -120,7 +122,7 @@ namespace CryptoExchange.Net.RateLimiting
else else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description); logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false); return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
@@ -133,6 +135,8 @@ namespace CryptoExchange.Net.RateLimiting
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight); var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
if (result.IsApplied) if (result.IsApplied)
{ {
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
if (type == RateLimitItemType.Connection) if (type == RateLimitItemType.Connection)
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current); logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
else else
@@ -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 /> /// <inheritdoc />
public Uri Uri => _request.RequestUri; public Uri Uri => _request.RequestUri!;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
+34 -23
View File
@@ -11,34 +11,13 @@ namespace CryptoExchange.Net.Requests
/// </summary> /// </summary>
public class RequestFactory : IRequestFactory public class RequestFactory : IRequestFactory
{ {
private HttpClient? _httpClient; private HttpClient? _httpClient;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
if (client == null) if (client == null)
{ client = CreateClient(proxy, requestTimeout);
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
catch (PlatformNotSupportedException) { }
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
client = new HttpClient(handler)
{
Timeout = requestTimeout
};
}
_httpClient = client; _httpClient = client;
} }
@@ -51,5 +30,37 @@ namespace CryptoExchange.Net.Requests
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
} }
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
{
_httpClient = CreateClient(proxy, requestTimeout);
}
private 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;
}
} }
} }
@@ -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 else
{ {
if (param.Names.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -113,13 +113,13 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (!string.IsNullOrEmpty(param.Name)) if (!string.IsNullOrEmpty(param.Name))
{ {
if (typeof(T).GetProperty(param.Name).GetValue(request, null) == null) if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
} }
else else
{ {
if (param.Names.All(x => typeof(T).GetProperty(param.Name).GetValue(request, null) == null)) if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; TimeFilterSupported = timeFilterSupported;
} }
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; TimeFilterSupported = timeFilterSupported;
} }
@@ -8,7 +8,7 @@
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// </summary>
public int? MaxTotalDataPoints { get; set; } public int? MaxTotalDataPoints { get; set; }
/// <summary> /// <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 /// The max age of the data that can be requested
/// </summary> /// </summary>
public TimeSpan? MaxAge { get; set; } public TimeSpan? MaxAge { get; set; }
@@ -31,14 +27,13 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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[] SupportIntervals = new[]
{ {
SharedKlineInterval.FiveMinutes, SharedKlineInterval.FiveMinutes,
SharedKlineInterval.FifteenMinutes, SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.OneHour, SharedKlineInterval.OneHour,
SharedKlineInterval.FifteenMinutes,
SharedKlineInterval.OneDay, SharedKlineInterval.OneDay,
SharedKlineInterval.OneWeek, SharedKlineInterval.OneWeek,
SharedKlineInterval.OneMonth SharedKlineInterval.OneMonth
@@ -48,7 +43,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; SupportIntervals = intervals;
} }
@@ -69,8 +64,8 @@ namespace CryptoExchange.Net.SharedApis
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} klines are available"); return new ArgumentError($"Only the most recent {MaxAge} klines are available");
if (MaxRequestDataPoints.HasValue && request.Limit > MaxRequestDataPoints.Value) if (request.Limit > MaxLimit)
return new ArgumentError($"Only {MaxRequestDataPoints} klines can be retrieved per request"); return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request");
if (MaxTotalDataPoints.HasValue) if (MaxTotalDataPoints.HasValue)
{ {
@@ -96,8 +91,6 @@ namespace CryptoExchange.Net.SharedApis
sb.AppendLine($"Max age of data: {MaxAge}"); sb.AppendLine($"Max age of data: {MaxAge}");
if (MaxTotalDataPoints != null) if (MaxTotalDataPoints != null)
sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}"); sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}");
if (MaxRequestDataPoints != null)
sb.AppendLine($"Max data points per request: {MaxRequestDataPoints}");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
public override string ToString(string exchange) public override string ToString(string exchange)
{ {
var sb = new StringBuilder(base.ToString(exchange)); var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Supported limit values: [{(SupportedLimits == null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]"); sb.AppendLine($"Supported limit values: [{(SupportedLimits != null ? string.Join(", ", SupportedLimits) : $"{MinLimit}..{MaxLimit}")}]");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -8,7 +8,7 @@
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </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; TimeFilterSupported = timeFilterSupported;
} }
@@ -1,4 +1,5 @@
using System.Text; using CryptoExchange.Net.Objects;
using System.Text;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
@@ -13,12 +14,24 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public SharedPaginationSupport PaginationSupport { get; } 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> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(needsAuthentication) public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
{ {
PaginationSupport = paginationType; PaginationSupport = paginationType;
TimePeriodFilterSupport = timePeriodSupport;
MaxLimit = maxLimit;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -26,6 +39,8 @@ namespace CryptoExchange.Net.SharedApis
{ {
var sb = new StringBuilder(base.ToString(exchange)); var sb = new StringBuilder(base.ToString(exchange));
sb.AppendLine($"Pagination type: {PaginationSupport}"); sb.AppendLine($"Pagination type: {PaginationSupport}");
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
sb.AppendLine($"Max limit: {MaxLimit}");
return sb.ToString(); return sb.ToString();
} }
} }
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
if (Name != null) if (Name != null)
return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {Name}: {Description} | example: {ExampleValue}";
return $"[{ValueType.Name}] {string.Join(" / ", Names)}: {Description} | example: {ExampleValue}"; return $"[{ValueType.Name}] {string.Join(" / ", Names!)}: {Description} | example: {ExampleValue}";
} }
} }
} }
@@ -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;
}
}
}
@@ -155,6 +155,12 @@ namespace CryptoExchange.Net.Sockets
_baseAddress = $"{Uri.Scheme}://{Uri.Host}"; _baseAddress = $"{Uri.Scheme}://{Uri.Host}";
} }
/// <inheritdoc />
public void UpdateProxy(ApiProxy? proxy)
{
Parameters.Proxy = proxy;
}
/// <inheritdoc /> /// <inheritdoc />
public virtual async Task<CallResult> ConnectAsync() public virtual async Task<CallResult> ConnectAsync()
{ {
@@ -189,9 +195,12 @@ namespace CryptoExchange.Net.Sockets
socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize);
if (Parameters.Proxy != null) if (Parameters.Proxy != null)
SetProxy(socket, Parameters.Proxy); SetProxy(socket, Parameters.Proxy);
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
socket.Options.CollectHttpResponseDetails = true; socket.Options.CollectHttpResponseDetails = true;
#endif #endif
#if NET9_0_OR_GREATER
socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
#endif
} }
catch (PlatformNotSupportedException) catch (PlatformNotSupportedException)
{ {
@@ -229,13 +238,13 @@ namespace CryptoExchange.Net.Sockets
if (e is WebSocketException we) if (e is WebSocketException we)
{ {
#if (NET6_0_OR_GREATER) #if (NET6_0_OR_GREATER)
if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests)
{ {
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#else #else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead // Try to read 429 from the message instead
if (we.Message.Contains("429")) if (we.Message.Contains("429"))
@@ -243,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message)); return new CallResult(new ServerRateLimitError(we.Message));
} }
#endif #endif
} }
return new CallResult(new CantConnectError()); return new CallResult(new CantConnectError());
@@ -435,8 +444,8 @@ namespace CryptoExchange.Net.Sockets
{ {
// Wait until we receive close confirmation // Wait until we receive close confirmation
await Task.Delay(10).ConfigureAwait(false); await Task.Delay(10).ConfigureAwait(false);
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5)) if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(1))
break; // Wait for max 5 seconds, then just abort the connection break; // Wait for max 1 second, then just abort the connection
} }
} }
} }
@@ -598,14 +607,14 @@ namespace CryptoExchange.Net.Sockets
if (_socket.State == WebSocketState.CloseReceived) if (_socket.State == WebSocketState.CloseReceived)
{ {
// Close received means it server initiated, we should send a confirmation and close the socket // Close received means it server initiated, we should send a confirmation and close the socket
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
if (_closeTask?.IsCompleted != false) if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
} }
else else
{ {
// Means the socket is now closed and we were the one initiating it // Means the socket is now closed and we were the one initiating it
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription); _logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString()!, receiveResult.CloseStatusDescription ?? string.Empty);
} }
break; break;
@@ -620,7 +629,7 @@ namespace CryptoExchange.Net.Sockets
// Write the data to a memory stream to be reassembled later // Write the data to a memory stream to be reassembled later
if (multipartStream == null) if (multipartStream == null)
multipartStream = new MemoryStream(); multipartStream = new MemoryStream();
multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
else else
{ {
@@ -634,7 +643,7 @@ namespace CryptoExchange.Net.Sockets
{ {
// Received the end of a multipart message, write to memory stream for reassembling // Received the end of a multipart message, write to memory stream for reassembling
_logger.SocketReceivedPartialMessage(Id, receiveResult.Count); _logger.SocketReceivedPartialMessage(Id, receiveResult.Count);
multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count); multipartStream!.Write(buffer.Array!, buffer.Offset, receiveResult.Count);
} }
break; break;
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Callback after query /// Callback after query
/// </summary> /// </summary>
public Action<CallResult>? Callback { get; set; } public Action<SocketConnection, CallResult>? Callback { get; set; }
} }
} }
+5
View File
@@ -23,6 +23,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool Completed { get; set; } public bool Completed { get; set; }
/// <summary>
/// Timeout for the request
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
/// <summary> /// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response /// and each symbol receives it's own confirmation response
+32 -6
View File
@@ -11,6 +11,8 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using System.Threading; using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -396,7 +398,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId) protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -425,7 +427,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="requestId">Id of the request sent</param> /// <param name="requestId">Id of the request sent</param>
protected virtual Task HandleRequestSentAsync(int requestId) protected virtual Task HandleRequestSentAsync(int requestId)
{ {
Query query; Query? query;
lock (_listenersLock) lock (_listenersLock)
{ {
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId); query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
@@ -437,7 +439,7 @@ namespace CryptoExchange.Net.Sockets
return Task.CompletedTask; return Task.CompletedTask;
} }
query.IsSend(ApiClient.ClientOptions.RequestTimeout); query.IsSend(query.RequestTimeout ?? ApiClient.ClientOptions.RequestTimeout);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -583,6 +585,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false); public async Task TriggerReconnectAsync() => await _socket.ReconnectAsync().ConfigureAwait(false);
/// <summary>
/// Update the proxy setting and reconnect
/// </summary>
/// <param name="proxy">New proxy setting</param>
public async Task UpdateProxy(ApiProxy? proxy)
{
_socket.UpdateProxy(proxy);
await TriggerReconnectAsync().ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Close the connection /// Close the connection
/// </summary> /// </summary>
@@ -615,6 +627,10 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task CloseAsync(Subscription subscription) 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; subscription.Closed = true;
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
@@ -898,7 +914,7 @@ namespace CryptoExchange.Net.Sockets
List<Subscription> subList; List<Subscription> subList;
lock (_listenersLock) 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) if (subList.Count == 0)
break; break;
@@ -907,20 +923,30 @@ namespace CryptoExchange.Net.Sockets
foreach (var subscription in subList) foreach (var subscription in subList)
{ {
subscription.ConnectionInvocations = 0; subscription.ConnectionInvocations = 0;
if (subscription.Closed)
// Can be closed during resubscribing
continue;
subscription.IsResubscribing = true;
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result) if (!result)
{ {
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
subscription.IsResubscribing = false;
return result; return result;
} }
var subQuery = subscription.GetSubQuery(this); var subQuery = subscription.GetSubQuery(this);
if (subQuery == null) if (subQuery == null)
{
subscription.IsResubscribing = false;
continue; continue;
}
var waitEvent = new AsyncResetEvent(false); var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{ {
subscription.IsResubscribing = false;
subscription.HandleSubQueryResponse(subQuery.Response!); subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set(); waitEvent.Set();
if (r.Result.Success) if (r.Result.Success)
@@ -974,7 +1000,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="interval">How often</param> /// <param name="interval">How often</param>
/// <param name="queryDelegate">Method returning the query to send</param> /// <param name="queryDelegate">Method returning the query to send</param>
/// <param name="callback">The callback for processing the response</param> /// <param name="callback">The callback for processing the response</param>
public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback) public virtual void QueryPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
if (queryDelegate == null) if (queryDelegate == null)
throw new ArgumentNullException(nameof(queryDelegate)); throw new ArgumentNullException(nameof(queryDelegate));
@@ -1006,7 +1032,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false); var result = await SendAndWaitQueryAsync(query).ConfigureAwait(false);
callback?.Invoke(result); callback?.Invoke(this, result);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -44,6 +44,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool Closed { get; set; } public bool Closed { get; set; }
/// <summary>
/// Is the subscription currently resubscribing
/// </summary>
public bool IsResubscribing { get; set; }
/// <summary> /// <summary>
/// Logger /// Logger
/// </summary> /// </summary>
@@ -378,7 +378,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal) if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString())) if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
} }
} }
@@ -211,7 +211,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object) if (dictProp.Value.Type == JTokenType.Object)
{ {
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties); CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties);
} }
else else
{ {
@@ -224,6 +224,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable)) else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
&& propertyValue.GetType() != typeof(string)) && propertyValue.GetType() != typeof(string))
{ {
if (propValue.Type != JTokenType.Array)
return;
var jObjs = (JArray)propValue; var jObjs = (JArray)propValue;
var list = (IEnumerable)propertyValue; var list = (IEnumerable)propertyValue;
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
@@ -372,7 +375,8 @@ namespace CryptoExchange.Net.Testing.Comparers
} }
else if (objectValue is DateTime time) 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}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
} }
else if (objectValue is bool bl) else if (objectValue is bool bl)
@@ -5,8 +5,11 @@ namespace CryptoExchange.Net.Testing
{ {
internal class EnumValueTraceListener : TraceListener internal class EnumValueTraceListener : TraceListener
{ {
public override void Write(string message) public override void Write(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -14,8 +17,11 @@ namespace CryptoExchange.Net.Testing
throw new Exception("Enum null error: " + message); throw new Exception("Enum null error: " + message);
} }
public override void WriteLine(string message) public override void WriteLine(string? message)
{ {
if (message == null)
return;
if (message.Contains("Cannot map")) if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message); throw new Exception("Enum value error: " + message);
@@ -25,5 +25,7 @@ namespace CryptoExchange.Net.Testing.Implementations
_request.RequestId = requestId; _request.RequestId = requestId;
return _request; return _request;
} }
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
} }
} }
@@ -92,5 +92,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public Task ReconnectAsync() => throw new NotImplementedException(); public Task ReconnectAsync() => throw new NotImplementedException();
public void Dispose() { } public void Dispose() { }
public void UpdateProxy(ApiProxy? proxy) => throw new NotImplementedException();
} }
} }
+11 -10
View File
@@ -169,21 +169,22 @@ namespace CryptoExchange.Net.Testing
{ {
var assembly = Assembly.GetAssembly(clientType); var assembly = Assembly.GetAssembly(clientType);
var interfaceType = clientType.GetInterface("I" + clientType.Name); var interfaceType = clientType.GetInterface("I" + clientType.Name);
var clientInterfaces = assembly.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared")); var clientInterfaces = assembly!.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name) && !t.Name.EndsWith("Shared"));
foreach (var clientInterface in clientInterfaces) foreach (var clientInterface in clientInterfaces)
{ {
var implementation = assembly.GetTypes().Single(t => clientInterface.IsAssignableFrom(t) && t != clientInterface); var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
int methods = 0; foreach (var implementation in implementations)
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
{ {
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()); int methods = 0;
if (interfaceMethod == null) foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}"); {
methods++; var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) ?? throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
} methods++;
}
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated"); Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
}
} }
} }
} }
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Trackers.Klines
/// <summary> /// <summary>
/// The internal data structure /// The internal data structure
/// </summary> /// </summary>
protected readonly Dictionary<DateTime, SharedKline> _data = new Dictionary<DateTime, SharedKline>(); protected readonly SortedDictionary<DateTime, SharedKline> _data = new SortedDictionary<DateTime, SharedKline>();
/// <summary> /// <summary>
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set /// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
/// </summary> /// </summary>
@@ -229,7 +229,7 @@ namespace CryptoExchange.Net.Trackers.Klines
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime) if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value); startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value);
var limit = Math.Min(_restClient.GetKlinesOptions.MaxRequestDataPoints ?? _restClient.GetKlinesOptions.MaxTotalDataPoints ?? 100, Limit ?? 100); var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit); var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit);
var data = new List<SharedKline>(); var data = new List<SharedKline>();
@@ -350,7 +350,8 @@ namespace CryptoExchange.Net.Trackers.Trades
_data.Add(item); _data.Add(item);
} }
_firstTimestamp = _data.Min(v => v.Timestamp); if (_data.Any())
_firstTimestamp = _data.Min(v => v.Timestamp);
ApplyWindow(false); ApplyWindow(false);
} }
+16 -15
View File
@@ -5,22 +5,23 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" /> <PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="Bitfinex.Net" Version="7.9.0" /> <PackageReference Include="Bitfinex.Net" Version="7.10.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" /> <PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="Bybit.Net" Version="3.15.0" /> <PackageReference Include="Bybit.Net" Version="3.16.0" />
<PackageReference Include="CoinEx.Net" Version="7.8.0" /> <PackageReference Include="CoinEx.Net" Version="7.9.0" />
<PackageReference Include="CryptoCom.Net" Version="1.1.0" /> <PackageReference Include="CryptoCom.Net" Version="1.2.0" />
<PackageReference Include="GateIo.Net" Version="1.10.0" /> <PackageReference Include="GateIo.Net" Version="1.12.0" />
<PackageReference Include="JK.BingX.Net" Version="1.12.0" /> <PackageReference Include="JK.BingX.Net" Version="1.14.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.11.0" /> <PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.10.0" /> <PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" /> <PackageReference Include="JK.OKX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.2.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.3.0" /> <PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.1.0" /> <PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
<PackageReference Include="Kucoin.Net" Version="5.17.0" /> <PackageReference Include="Kucoin.Net" Version="5.18.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="WhiteBit.Net" Version="1.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+9 -1
View File
@@ -14,6 +14,7 @@
@inject IKucoinRestClient kucoinClient @inject IKucoinRestClient kucoinClient
@inject IMexcRestClient mexcClient @inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IWhiteBitRestClient whitebitClient
<h3>BTC-USD prices:</h3> <h3>BTC-USD prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key)) @foreach(var price in _prices.OrderBy(p => p.Key))
@@ -41,12 +42,13 @@
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT"); var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask); await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
if (binanceTask.Result.Success) if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice); _prices.Add("Binance", binanceTask.Result.Data.LastPrice);
if (bingXTask.Result.Success) if (bingXTask.Result.Success)
_prices.Add("BingX", bingXTask.Result.Data.First().LastPrice); _prices.Add("BingX", bingXTask.Result.Data.First().LastPrice);
@@ -88,6 +90,12 @@
if (okxTask.Result.Success) if (okxTask.Result.Success)
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0); _prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
if (whitebitTask.Result.Success){
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
var tickers = whitebitTask.Result.Data;
_prices.Add("WhiteBit", tickers.Single(x => x.Symbol == "BTC_USDT").LastPrice);
}
} }
} }
@@ -14,6 +14,7 @@
@inject IKucoinSocketClient kucoinSocketClient @inject IKucoinSocketClient kucoinSocketClient
@inject IMexcSocketClient mexcSocketClient @inject IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient @inject IOKXSocketClient okxSocketClient
@inject IWhiteBitSocketClient whitebitSocketClient
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using CryptoExchange.Net.Objects @using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Objects.Sockets; @using CryptoExchange.Net.Objects.Sockets;
@@ -49,6 +50,7 @@
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)), kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)), mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)), okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
}; };
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
+4 -1
View File
@@ -18,6 +18,7 @@
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@inject IBinanceOrderBookFactory binanceFactory @inject IBinanceOrderBookFactory binanceFactory
@inject IBingXOrderBookFactory bingXFactory @inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory @inject IBitfinexOrderBookFactory bitfinexFactory
@@ -33,6 +34,7 @@
@inject IKucoinOrderBookFactory kucoinFactory @inject IKucoinOrderBookFactory kucoinFactory
@inject IMexcOrderBookFactory mexcFactory @inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory @inject IOKXOrderBookFactory okxFactory
@inject IWhiteBitOrderBookFactory whitebitFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC books, live updates:</h3> <h3>ETH-BTC books, live updates:</h3>
@@ -77,10 +79,11 @@
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") }, { "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") }, { "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
{ "HTX", htxFactory.CreateSpot("ethbtc") }, { "HTX", htxFactory.CreateSpot("ethbtc") },
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") }, { "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") }, { "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") }, { "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") }, { "OKX", okxFactory.Create("ETH-BTC") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
}; };
await Task.WhenAll(_books.Select(b => b.Value.StartAsync())); await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
@@ -20,6 +20,7 @@
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@inject IBinanceTrackerFactory binanceFactory @inject IBinanceTrackerFactory binanceFactory
@inject IBingXTrackerFactory bingXFactory @inject IBingXTrackerFactory bingXFactory
@inject IBitfinexTrackerFactory bitfinexFactory @inject IBitfinexTrackerFactory bitfinexFactory
@@ -35,6 +36,7 @@
@inject IKucoinTrackerFactory kucoinFactory @inject IKucoinTrackerFactory kucoinFactory
@inject IMexcTrackerFactory mexcFactory @inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory @inject IOKXTrackerFactory okxFactory
@inject IWhiteBitTrackerFactory whitebitFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC trade Trackers, live updates:</h3> <h3>ETH-BTC trade Trackers, live updates:</h3>
@@ -76,6 +78,7 @@
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ whitebitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
}; };
await Task.WhenAll(_trackers.Select(b => b.StartAsync())); await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
+1
View File
@@ -50,6 +50,7 @@ namespace BlazorClient
services.AddKucoin(); services.AddKucoin();
services.AddMexc(); services.AddMexc();
services.AddOKX(); services.AddOKX();
services.AddWhiteBit();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+1
View File
@@ -23,4 +23,5 @@
@using Kucoin.Net.Interfaces.Clients; @using Kucoin.Net.Interfaces.Clients;
@using Mexc.Net.Interfaces.Clients; @using Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using WhiteBit.Net.Interfaces.Clients
@using CryptoExchange.Net.Interfaces; @using CryptoExchange.Net.Interfaces;
+14 -14
View File
@@ -6,20 +6,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" /> <PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="Bitfinex.Net" Version="7.9.0" /> <PackageReference Include="Bitfinex.Net" Version="7.10.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" /> <PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="Bybit.Net" Version="3.15.0" /> <PackageReference Include="Bybit.Net" Version="3.16.0" />
<PackageReference Include="CoinEx.Net" Version="7.8.0" /> <PackageReference Include="CoinEx.Net" Version="7.9.0" />
<PackageReference Include="CryptoCom.Net" Version="1.1.0" /> <PackageReference Include="CryptoCom.Net" Version="1.2.0" />
<PackageReference Include="GateIo.Net" Version="1.10.0" /> <PackageReference Include="GateIo.Net" Version="1.12.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.11.0" /> <PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.10.0" /> <PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" /> <PackageReference Include="JK.OKX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.2.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.3.0" /> <PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.1.0" /> <PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
<PackageReference Include="Kucoin.Net" Version="5.17.0" /> <PackageReference Include="Kucoin.Net" Version="5.18.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -3
View File
@@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" /> <PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" /> <PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" /> <PackageReference Include="JK.OKX.Net" Version="2.8.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+32
View File
@@ -0,0 +1,32 @@
{
// Options section, select this section during DI registration using Configuration.GetSection("ExchangeApiOptions")
"ExchangeApiOptions": {
// API credentials for both REST and Websocket client
"ApiCredentials": {
"Key": "APIKEY",
"Secret": "SECRET",
"PassPhrase": "Phrase" // Optional passphrase for exchanges which need it
},
// Set the environment by name
"Environment": {
"name": "live"
},
// REST client options
"Rest": {
"RequestTimeout": "00:00:20",
"CachingEnabled": true,
"OutputOriginalData": true,
"Proxy": {
"Host": "https://127.0.0.1",
"Port": 8080,
"Login": "User",
"Password": "Pass"
}
},
// Socket client options
"Socket": {
"RequestTimeout": "00:00:05",
"SocketSubscriptionsCombineTarget": 15
}
}
}
+59
View File
@@ -28,6 +28,8 @@ The following API's are directly supported. Note that there are 3rd party implem
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)| |Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)|
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)| |Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)| |OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|
|WhiteBit|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|
|XT|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's. Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
@@ -38,6 +40,21 @@ A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free t
## Support the project ## Support the project
Any support is greatly appreciated. Any support is greatly appreciated.
## Referral
When creating an account on new exchanges please consider using a referral link from below to support development
|Exchange|Link|
|--|--|
|Bybit|[https://partner.bybit.com/b/jkorf](https://partner.bybit.com/b/jkorf)|
|Coinbase|[https://advanced.coinbase.com/join/T6H54H8](https://advanced.coinbase.com/join/T6H54H8)|
|CoinEx|[https://www.coinex.com/register?refer_code=hd6gn](https://www.coinex.com/register?refer_code=hd6gn)|
|Crypto.com|[https://crypto.com/exch/26ge92xbkn](https://crypto.com/exch/26ge92xbkn)|
|HTX|[https://www.htx.com/invite/en-us/1f?invite_code=fxp9](https://www.htx.com/invite/en-us/1f?invite_code=fxp9)|
|Kucoin|[https://www.kucoin.com/r/rf/QBS4FPED](https://www.kucoin.com/r/rf/QBS4FPED)|
|OKX|[https://okx.com/join/48046699](https://okx.com/join/48046699)|
|WhiteBit|[https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|
|XT|[https://www.xt.com/en/accounts/register?ref=1HRM5J](https://www.xt.com/en/accounts/register?ref=1HRM5J)|
### Donate ### Donate
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me. Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
@@ -49,6 +66,48 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 8.5.0 - 23 Dec 2024
* 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 timeout per request
* Added build target for net9.0
* Added setting of KeepAliveTimeout on websocket connections to improve dropped connection detection
* 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
* Version 8.4.5 - 20 Dec 2024
* Added EmptyArrayObjectConverter System.Text.Json JsonConverter
* Added JsonSerializerOptions parameter to SystemTextJsonMessageAccessor constructor
* Version 8.4.4 - 08 Dec 2024
* Changed JsonConverterCtorAttribute to use constructor type parameter instead of generic type parameter to support .net framework
* Version 8.4.3 - 03 Dec 2024
* Fixed KlineTracker update handling
* Version 8.4.2 - 02 Dec 2024
* Removed special characters in ClientOrderIdSeperator to adhere to field content rules
* Version 8.4.1 - 02 Dec 2024
* Added JsonConverterCtorAttribute to allow specifying a custom JsonConverter with constructor parameters on properties
* Added ReplaceConverter System.Text.Json converter
* Added LibraryHelpers class for internal helper methods
* Version 8.4.0 - 28 Nov 2024
* Added GetFeesAsync Shared REST client support
* Added LibraryOptions base class
* Added CommaSplitEnumConverter System.Text.Json converter
* Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions
* Updated package dependency versions
* Version 8.3.0 - 19 Nov 2024
* Added support for IOptions injection, allowing options to be read from IConfiguration
* Added handling of Infinity values in decimal converter
* Added rate limit update event
* Small refactor on client options internals
* Fixed concurrency issue when unsubscribing websocket subscription during reconnection
* Version 8.2.0 - 06 Nov 2024 * Version 8.2.0 - 06 Nov 2024
* Added support for not allowing duplicate subscription topics on the same websocket connection * Added support for not allowing duplicate subscription topics on the same websocket connection
* Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable * Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable
+764 -128
View File
File diff suppressed because it is too large Load Diff