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

Compare commits

..

7 Commits

Author SHA1 Message Date
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
37 changed files with 823 additions and 316 deletions
+21 -5
View File
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
[Test]
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
}
@@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
Environment = new TestEnvironment("test", "https://test.com")
};
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
/// <summary>
/// The default receive window for requests
/// </summary>
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy()
internal TestClientOptions Set(TestClientOptions targetOptions)
{
var options = Copy<TestClientOptions>();
options.Api1Options = Api1Options.Copy<RestApiOptions>();
options.Api2Options = Api2Options.Copy<RestApiOptions>();
return options;
targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions;
}
}
}
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
public TestBaseClient(): base(null, "Test")
{
var options = TestClientOptions.Default.Copy();
var options = new TestClientOptions();
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
@@ -16,6 +16,7 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -24,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
}
Initialize(options.Value);
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestClientOptions.Default.Copy();
optionsFunc(options);
Initialize(options);
Api1 = new TestRestApi1Client(options);
Api2 = new TestRestApi2Client(options);
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
using Moq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
@@ -22,25 +23,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public TestSubSocketClient SubClient { get; }
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
{
}
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
optionsFunc(options);
Initialize(options);
Initialize(options.Value);
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
@@ -70,7 +66,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
Environment = new TestEnvironment("Live", "https://test.test")
};
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
}
public class TestSubSocketClient : SocketApiClient
@@ -13,39 +13,30 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// The api key / label to authenticate requests
/// </summary>
public string Key { get; }
public string Key { get; set; }
/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; }
public string Secret { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; }
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
{
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="credentialsType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
CredentialType = credentialType;
Key = key;
Secret = secret;
}
@@ -65,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="inputStream">The stream containing the json data</param>
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
{
var accessor = new SystemTextJsonStreamMessageAccessor();
if (!accessor.Read(inputStream, false).Result)
@@ -75,11 +66,9 @@ namespace CryptoExchange.Net.Authentication
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
if (key == null || secret == null)
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
Key = key;
Secret = secret;
inputStream.Seek(0, SeekOrigin.Begin);
return new ApiCredentials(key, secret);
}
}
}
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Authentication
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key;
public string ApiKey => _credentials.Key!;
/// <summary>
/// ctor
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Authentication
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Secret == null)
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
_credentials = credentials;
+10
View File
@@ -109,6 +109,16 @@ namespace CryptoExchange.Net.Clients
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
@@ -19,9 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try
{
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
+4 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>8.2.0</PackageVersion>
<AssemblyVersion>8.2.0</AssemblyVersion>
<FileVersion>8.2.0</FileVersion>
<PackageVersion>8.3.0</PackageVersion>
<AssemblyVersion>8.3.0</AssemblyVersion>
<FileVersion>8.3.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -59,5 +59,6 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>
</Project>
+5 -14
View File
@@ -8,30 +8,21 @@
/// <summary>
/// The host address of the proxy
/// </summary>
public string Host { get; }
public string Host { get; set; }
/// <summary>
/// The port of the proxy
/// </summary>
public int Port { get; }
public int Port { get; set; }
/// <summary>
/// The login of the proxy
/// </summary>
public string? Login { get; }
public string? Login { get; set; }
/// <summary>
/// The password of the proxy
/// </summary>
public string? Password { get; }
/// <summary>
/// Create new settings for a proxy
/// </summary>
/// <param name="host">The proxy hostname/ip</param>
/// <param name="port">The proxy port</param>
public ApiProxy(string host, int port): this(host, port, null, null)
{
}
public string? Password { get; set; }
/// <summary>
/// Create new settings for a proxy
@@ -40,7 +31,7 @@
/// <param name="port">The proxy port</param>
/// <param name="login">The proxy login</param>
/// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, string? password)
public ApiProxy(string host, int port, string? login = null, string? password = null)
{
Host = host;
Port = port;
@@ -19,19 +19,15 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan? TimestampRecalculationInterval { get; set; }
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public virtual T Copy<T>() where T : RestApiOptions, new()
public T Set<T>(T item) where T : RestApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
return item;
}
}
@@ -29,25 +29,21 @@ namespace CryptoExchange.Net.Objects.Options
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : RestExchangeOptions, new()
public T Set<T>(T item) where T : RestExchangeOptions, new()
{
return new T
{
OutputOriginalData = OutputOriginalData,
AutoTimestamp = AutoTimestamp,
TimestampRecalculationInterval = TimestampRecalculationInterval,
ApiCredentials = ApiCredentials?.Copy(),
Proxy = Proxy,
RequestTimeout = RequestTimeout,
RateLimiterEnabled = RateLimiterEnabled,
RateLimitingBehaviour = RateLimitingBehaviour,
CachingEnabled = CachingEnabled,
CachingMaxAge = CachingMaxAge,
};
item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
item.ApiCredentials = ApiCredentials?.Copy();
item.Proxy = Proxy;
item.RequestTimeout = RequestTimeout;
item.RateLimiterEnabled = RateLimiterEnabled;
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge;
return item;
}
}
@@ -66,15 +62,13 @@ namespace CryptoExchange.Net.Objects.Options
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
{
var result = base.Copy<T>();
result.Environment = Environment;
return result;
base.Set(target);
target.Environment = Environment;
return target;
}
}
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
public int? MaxSocketConnections { get; set; }
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketApiOptions, new()
public T Set<T>(T item) where T : SocketApiOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
SocketNoDataTimeout = SocketNoDataTimeout,
MaxSocketConnections = MaxSocketConnections,
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.SocketNoDataTimeout = SocketNoDataTimeout;
item.MaxSocketConnections = MaxSocketConnections;
return item;
}
}
@@ -57,24 +57,22 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Copy<T>() where T : SocketExchangeOptions, new()
public T Set<T>(T item) where T : SocketExchangeOptions, new()
{
return new T
{
ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData,
ReconnectPolicy = ReconnectPolicy,
DelayAfterConnect = DelayAfterConnect,
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
ReconnectInterval = ReconnectInterval,
SocketNoDataTimeout = SocketNoDataTimeout,
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
MaxSocketConnections = MaxSocketConnections,
Proxy = Proxy,
RequestTimeout = RequestTimeout,
RateLimitingBehaviour = RateLimitingBehaviour,
RateLimiterEnabled = RateLimiterEnabled,
};
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData;
item.ReconnectPolicy = ReconnectPolicy;
item.DelayAfterConnect = DelayAfterConnect;
item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
item.ReconnectInterval = ReconnectInterval;
item.SocketNoDataTimeout = SocketNoDataTimeout;
item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
item.MaxSocketConnections = MaxSocketConnections;
item.Proxy = Proxy;
item.RequestTimeout = RequestTimeout;
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimiterEnabled = RateLimiterEnabled;
return item;
}
}
@@ -93,15 +91,13 @@ namespace CryptoExchange.Net.Objects.Options
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
/// <summary>
/// Create a copy of this options
/// Set the values of this options on the target options
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
{
var result = base.Copy<T>();
result.Environment = Environment;
return result;
base.Set(target);
target.Environment = Environment;
return target;
}
}
@@ -24,14 +24,14 @@
/// <summary>
/// Name of the environment
/// </summary>
public string EnvironmentName { get; init; }
public string Name { get; set; }
/// <summary>
/// </summary>
/// <param name="name"></param>
protected TradeEnvironment(string name)
{
EnvironmentName = name;
Name = name;
}
}
}
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
}
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded;
return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
}
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary>
event Action<RateLimitEvent> RateLimitTriggered;
/// <summary>
/// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying.
/// </summary>
event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary>
/// Add a rate limit guard
/// </summary>
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// No wait needed
/// </summary>
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current);
/// <summary>
/// Wait needed
@@ -4,10 +4,14 @@ using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit event
/// Rate limit triggered event
/// </summary>
public record RateLimitEvent
{
/// <summary>
/// Id of the item the limit was checked for
/// </summary>
public int ItemId { get; set; }
/// <summary>
/// Name of the API limit that is reached
/// </summary>
@@ -52,18 +56,9 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// ctor
/// </summary>
/// <param name="apiLimit"></param>
/// <param name="limitDescription"></param>
/// <param name="definition"></param>
/// <param name="host"></param>
/// <param name="current"></param>
/// <param name="requestWeight"></param>
/// <param name="limit"></param>
/// <param name="timePeriod"></param>
/// <param name="delayTime"></param>
/// <param name="behaviour"></param>
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
{
ItemId = itemId;
ApiLimit = apiLimit;
LimitDescription = limitDescription;
RequestDefinition = definition;
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.RateLimiting
/// <inheritdoc />
public event Action<RateLimitEvent>? RateLimitTriggered;
/// <inheritdoc />
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
/// <summary>
/// ctor
@@ -105,7 +107,7 @@ namespace CryptoExchange.Net.RateLimiting
else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
}
@@ -120,7 +122,7 @@ namespace CryptoExchange.Net.RateLimiting
else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
@@ -133,6 +135,8 @@ namespace CryptoExchange.Net.RateLimiting
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
if (result.IsApplied)
{
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
if (type == RateLimitItemType.Connection)
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
else
@@ -0,0 +1,50 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit update event
/// </summary>
public record RateLimitUpdateEvent
{
/// <summary>
/// Id of the item the limit was checked for
/// </summary>
public int ItemId { get; set; }
/// <summary>
/// Name of the API limit that is reached
/// </summary>
public string ApiLimit { get; set; } = string.Empty;
/// <summary>
/// Description of the limit that is reached
/// </summary>
public string LimitDescription { get; set; } = string.Empty;
/// <summary>
/// The current counter value
/// </summary>
public int Current { get; set; }
/// <summary>
/// The limit per time period
/// </summary>
public int? Limit { get; set; }
/// <summary>
/// The time period the limit is for
/// </summary>
public TimeSpan? TimePeriod { get; set; }
/// <summary>
/// ctor
/// </summary>
public RateLimitUpdateEvent(int itemId, string apiLimit, string limitDescription, int current, int? limit, TimeSpan? timePeriod)
{
ItemId = itemId;
ApiLimit = apiLimit;
LimitDescription = limitDescription;
Current = current;
Limit = limit;
TimePeriod = timePeriod;
}
}
}
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Requests
/// </summary>
public class RequestFactory : IRequestFactory
{
private HttpClient? _httpClient;
private HttpClient? _httpClient;
/// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
+15 -1
View File
@@ -615,6 +615,10 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public async Task CloseAsync(Subscription subscription)
{
// If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues
while (subscription.IsResubscribing)
await Task.Delay(50).ConfigureAwait(false);
subscription.Closed = true;
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
@@ -898,7 +902,7 @@ namespace CryptoExchange.Net.Sockets
List<Subscription> subList;
lock (_listenersLock)
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
if (subList.Count == 0)
break;
@@ -907,20 +911,30 @@ namespace CryptoExchange.Net.Sockets
foreach (var subscription in subList)
{
subscription.ConnectionInvocations = 0;
if (subscription.Closed)
// Can be closed during resubscribing
continue;
subscription.IsResubscribing = true;
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result)
{
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
subscription.IsResubscribing = false;
return result;
}
var subQuery = subscription.GetSubQuery(this);
if (subQuery == null)
{
subscription.IsResubscribing = false;
continue;
}
var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{
subscription.IsResubscribing = false;
subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set();
if (r.Result.Success)
@@ -44,6 +44,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool Closed { get; set; }
/// <summary>
/// Is the subscription currently resubscribing
/// </summary>
public bool IsResubscribing { get; set; }
/// <summary>
/// Logger
/// </summary>
+16 -15
View File
@@ -5,22 +5,23 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" />
<PackageReference Include="Bitfinex.Net" Version="7.9.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" />
<PackageReference Include="Bybit.Net" Version="3.15.0" />
<PackageReference Include="CoinEx.Net" Version="7.8.0" />
<PackageReference Include="CryptoCom.Net" Version="1.1.0" />
<PackageReference Include="GateIo.Net" Version="1.10.0" />
<PackageReference Include="JK.BingX.Net" Version="1.12.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.11.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.10.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.2.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.1.0" />
<PackageReference Include="Kucoin.Net" Version="5.17.0" />
<PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="Bitfinex.Net" Version="7.10.0" />
<PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="Bybit.Net" Version="3.16.0" />
<PackageReference Include="CoinEx.Net" Version="7.9.0" />
<PackageReference Include="CryptoCom.Net" Version="1.2.0" />
<PackageReference Include="GateIo.Net" Version="1.12.0" />
<PackageReference Include="JK.BingX.Net" Version="1.14.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
<PackageReference Include="Kucoin.Net" Version="5.18.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="WhiteBit.Net" Version="1.0.0" />
</ItemGroup>
</Project>
+9 -1
View File
@@ -14,6 +14,7 @@
@inject IKucoinRestClient kucoinClient
@inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient
@inject IWhiteBitRestClient whitebitClient
<h3>BTC-USD prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key))
@@ -41,12 +42,13 @@
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var mexcTask = mexcClient.SpotApi.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);
if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
if (bingXTask.Result.Success)
_prices.Add("BingX", bingXTask.Result.Data.First().LastPrice);
@@ -88,6 +90,12 @@
if (okxTask.Result.Success)
_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 IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient
@inject IWhiteBitSocketClient whitebitSocketClient
@using System.Collections.Concurrent
@using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Objects.Sockets;
@@ -49,6 +50,7 @@
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
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);
+4 -1
View File
@@ -18,6 +18,7 @@
@using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces
@using OKX.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@inject IBinanceOrderBookFactory binanceFactory
@inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory
@@ -33,6 +34,7 @@
@inject IKucoinOrderBookFactory kucoinFactory
@inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory
@inject IWhiteBitOrderBookFactory whitebitFactory
@implements IDisposable
<h3>ETH-BTC books, live updates:</h3>
@@ -77,10 +79,11 @@
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
{ "HTX", htxFactory.CreateSpot("ethbtc") },
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
};
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
@@ -20,6 +20,7 @@
@using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces
@using OKX.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@inject IBinanceTrackerFactory binanceFactory
@inject IBingXTrackerFactory bingXFactory
@inject IBitfinexTrackerFactory bitfinexFactory
@@ -35,6 +36,7 @@
@inject IKucoinTrackerFactory kucoinFactory
@inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory
@inject IWhiteBitTrackerFactory whitebitFactory
@implements IDisposable
<h3>ETH-BTC trade Trackers, live updates:</h3>
@@ -76,6 +78,7 @@
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.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()));
+1
View File
@@ -50,6 +50,7 @@ namespace BlazorClient
services.AddKucoin();
services.AddMexc();
services.AddOKX();
services.AddWhiteBit();
}
// 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 Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
@using WhiteBit.Net.Interfaces.Clients
@using CryptoExchange.Net.Interfaces;
+14 -14
View File
@@ -6,20 +6,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" />
<PackageReference Include="Bitfinex.Net" Version="7.9.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" />
<PackageReference Include="Bybit.Net" Version="3.15.0" />
<PackageReference Include="CoinEx.Net" Version="7.8.0" />
<PackageReference Include="CryptoCom.Net" Version="1.1.0" />
<PackageReference Include="GateIo.Net" Version="1.10.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.11.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.10.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.2.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.1.0" />
<PackageReference Include="Kucoin.Net" Version="5.17.0" />
<PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="Bitfinex.Net" Version="7.10.0" />
<PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="Bybit.Net" Version="3.16.0" />
<PackageReference Include="CoinEx.Net" Version="7.9.0" />
<PackageReference Include="CryptoCom.Net" Version="1.2.0" />
<PackageReference Include="GateIo.Net" Version="1.12.0" />
<PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
<PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
<PackageReference Include="Kucoin.Net" Version="5.18.0" />
</ItemGroup>
</Project>
+3 -3
View File
@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="10.8.0" />
<PackageReference Include="BitMart.Net" Version="1.5.0" />
<PackageReference Include="JK.OKX.Net" Version="2.7.0" />
<PackageReference Include="Binance.Net" Version="10.9.0" />
<PackageReference Include="BitMart.Net" Version="1.7.0" />
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
</ItemGroup>
</Project>
+26
View File
@@ -0,0 +1,26 @@
{
"ExchangeApiOptions": {
"ApiCredentials": {
"Key": "APIKEY",
"Secret": "SECRET"
},
"Environment": {
"name": "live"
},
"Rest":{
"RequestTimeout": "00:00:20",
"CachingEnabled": true,
"OutputOriginalData": true,
"Proxy": {
"Host": "https://127.0.0.1",
"Port": 8080,
"Login": "User",
"Password": "Pass"
}
},
"Socket":{
"RequestTimeout": "00:00:05",
"SocketSubscriptionsCombineTarget": 15
}
}
}
+8
View File
@@ -28,6 +28,7 @@ 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)|
|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)|
|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)|
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
@@ -49,6 +50,13 @@ 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).
## Release notes
* 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
* Added support for not allowing duplicate subscription topics on the same websocket connection
* Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable
+505 -124
View File
File diff suppressed because it is too large Load Diff