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

Compare commits

...

18 Commits

Author SHA1 Message Date
JKorf b34129e148 Updated version 2023-08-24 21:25:37 +02:00
JKorf be25a68c9c Ratelimiting for socket requests 2023-08-24 20:51:17 +02:00
JKorf 468cd5e48e Added RetryAfter property for ratelimit errors, added parsing of rate limit return 2023-08-21 21:34:26 +02:00
JKorf 262c4e4aa5 Dont process unsubscribe if there are no subscriptions 2023-08-21 20:11:31 +02:00
Jan Korf 4ccff6461f Merge pull request #177 from ASolomatin/master
Ability for all Error derived classes to have Code and Data
2023-08-21 20:08:50 +02:00
JKorf 3bfa3ef389 Update index.md 2023-08-04 22:50:17 +02:00
JKorf 5238971bcc Update index.md 2023-08-04 22:44:09 +02:00
JKorf 2f5c904faf Added okx 2023-08-04 22:38:21 +02:00
JKorf c62775813f Updated version 2023-07-23 13:52:07 +02:00
JKorf f11b3754f0 Fix for proxy when not using DI 2023-07-23 10:01:13 +02:00
JKorf 3cbe0465e9 Docs 2023-07-11 21:38:48 +02:00
JKorf 5048aea722 Updated version 2023-07-05 21:58:35 +02:00
Aleksej Solomatin 8d35339ab2 Ability for all Error derived classes to have Code and Data
Proposal https://github.com/JKorf/CryptoExchange.Net/issues/176
2023-07-05 19:39:04 +03:00
Jkorf c3316a51e7 Added properties dictionary to socket connection 2023-07-05 17:10:43 +02:00
JKorf 7ecf37064b Updated version 2023-06-29 20:10:35 +02:00
JKorf 18954f4f53 Added optional log level parameter for trace logger 2023-06-29 20:07:26 +02:00
JKorf 00bc245102 Updated examples 2023-06-26 20:45:28 +02:00
JKorf 273cab9fdb Docs 2023-06-25 21:43:30 +02:00
41 changed files with 754 additions and 817 deletions
@@ -140,6 +140,12 @@ namespace CryptoExchange.Net.UnitTests
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null); var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
client.SubClient.ConnectSocketSub(sub1); client.SubClient.ConnectSocketSub(sub1);
client.SubClient.ConnectSocketSub(sub2); client.SubClient.ConnectSocketSub(sub2);
var us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
sub1.AddSubscription(us1);
sub2.AddSubscription(us2);
var ups1 = new UpdateSubscription(sub1, us1);
var ups2 = new UpdateSubscription(sub2, us2);
// act // act
client.UnsubscribeAllAsync().Wait(); client.UnsubscribeAllAsync().Wait();
@@ -182,9 +182,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct); return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
} }
protected override Error ParseErrorResponse(JToken error) protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{ {
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]); var errorData = ValidateJson(data);
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
} }
public override TimeSpan? GetTimeOffset() public override TimeSpan? GetTimeOffset()
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public event Action OnReconnected; public event Action OnReconnected;
public event Action OnReconnecting; public event Action OnReconnecting;
#pragma warning restore 0067 #pragma warning restore 0067
public event Action<int> OnRequestSent;
public event Action<string> OnMessage; public event Action<string> OnMessage;
public event Action<Exception> OnError; public event Action<Exception> OnError;
public event Action OnOpen; public event Action OnOpen;
@@ -69,10 +70,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return Task.FromResult(CanConnect); return Task.FromResult(CanConnect);
} }
public void Send(string data) public void Send(int requestId, string data, int weight)
{ {
if(!Connected) if(!Connected)
throw new Exception("Socket not connected"); throw new Exception("Socket not connected");
OnRequestSent?.Invoke(requestId);
} }
public void Reset() public void Reset()
@@ -77,15 +77,6 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public bool OutputOriginalData { get; } public bool OutputOriginalData { get; }
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// </summary>
protected static int _lastId;
/// <summary>
/// Lock for id generating
/// </summary>
protected static object _idLock = new();
/// <summary> /// <summary>
/// A default serializer /// A default serializer
/// </summary> /// </summary>
@@ -338,19 +329,6 @@ namespace CryptoExchange.Net
return await reader.ReadToEndAsync().ConfigureAwait(false); return await reader.ReadToEndAsync().ConfigureAwait(false);
} }
/// <summary>
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
/// </summary>
/// <returns></returns>
protected static int NextId()
{
lock (_idLock)
{
_lastId += 1;
return _lastId;
}
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+41 -8
View File
@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,6 +15,7 @@ using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using static CryptoExchange.Net.Objects.RateLimiter;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -71,7 +73,7 @@ namespace CryptoExchange.Net
rateLimiters.Add(rateLimiter); rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters; RateLimiters = rateLimiters;
RequestFactory.Configure(options.RequestTimeout, httpClient); RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
} }
/// <summary> /// <summary>
@@ -194,7 +196,7 @@ namespace CryptoExchange.Net
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false) bool ignoreRatelimit = false)
{ {
var requestId = NextId(); var requestId = ExchangeHelpers.NextId();
if (signed) if (signed)
{ {
@@ -344,8 +346,13 @@ namespace CryptoExchange.Net
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}"); _logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
responseStream.Close(); responseStream.Close();
response.Close(); response.Close();
var parseResult = ValidateJson(data);
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!; Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
else
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
if (error.Code == null || error.Code == 0) if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode; error.Code = (int)response.StatusCode;
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
@@ -529,13 +536,39 @@ namespace CryptoExchange.Net
} }
/// <summary> /// <summary>
/// Parse an error response from the server. Only used when server returns a status other than Success(200) /// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
/// </summary> /// </summary>
/// <param name="error">The string the request returned</param> /// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="data">The response data</param>
/// <returns></returns> /// <returns></returns>
protected virtual Error ParseErrorResponse(JToken error) protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{ {
return new ServerError(error.ToString()); return new ServerError(data);
}
/// <summary>
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="data">The response data</param>
/// <returns></returns>
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (!retryAfterHeader.Value.Any())
return new ServerRateLimitError(data);
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError(data) { RetryAfter = datetime };
return new ServerRateLimitError(data);
} }
/// <summary> /// <summary>
+27 -15
View File
@@ -12,6 +12,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static CryptoExchange.Net.Objects.RateLimiter;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -76,9 +77,9 @@ namespace CryptoExchange.Net
protected internal bool UnhandledMessageExpected { get; set; } protected internal bool UnhandledMessageExpected { get; set; }
/// <summary> /// <summary>
/// The max amount of outgoing messages per socket per second /// The rate limiters
/// </summary> /// </summary>
protected internal int? RateLimitPerSocketPerSecond { get; set; } protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
@@ -130,6 +131,10 @@ namespace CryptoExchange.Net
options, options,
apiOptions) apiOptions)
{ {
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters;
} }
/// <summary> /// <summary>
@@ -275,7 +280,7 @@ namespace CryptoExchange.Net
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription) protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{ {
CallResult<object>? callResult = null; CallResult<object>? callResult = null;
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false); await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, 1, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true) if (callResult?.Success == true)
{ {
@@ -295,10 +300,11 @@ namespace CryptoExchange.Net
/// <typeparam name="T">Expected result type</typeparam> /// <typeparam name="T">Expected result type</typeparam>
/// <param name="request">The request to send, will be serialized to json</param> /// <param name="request">The request to send, will be serialized to json</param>
/// <param name="authenticated">If the query is to an authenticated endpoint</param> /// <param name="authenticated">If the query is to an authenticated endpoint</param>
/// <param name="weight">Weight of the request</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated) protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated, int weight = 1)
{ {
return QueryAsync<T>(BaseAddress, request, authenticated); return QueryAsync<T>(BaseAddress, request, authenticated, weight);
} }
/// <summary> /// <summary>
@@ -308,8 +314,9 @@ namespace CryptoExchange.Net
/// <param name="url">The url for the request</param> /// <param name="url">The url for the request</param>
/// <param name="request">The request to send</param> /// <param name="request">The request to send</param>
/// <param name="authenticated">Whether the socket should be authenticated</param> /// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="weight">Weight of the request</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated) protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated, int weight = 1)
{ {
if (_disposing) if (_disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query")); return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
@@ -348,7 +355,7 @@ namespace CryptoExchange.Net
return new CallResult<T>(new ServerError("Socket is paused")); return new CallResult<T>(new ServerError("Socket is paused"));
} }
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false); return await QueryAndWaitAsync<T>(socketConnection, request, weight).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -357,11 +364,12 @@ namespace CryptoExchange.Net
/// <typeparam name="T">The expected result type</typeparam> /// <typeparam name="T">The expected result type</typeparam>
/// <param name="socket">The connection to send and wait on</param> /// <param name="socket">The connection to send and wait on</param>
/// <param name="request">The request to send</param> /// <param name="request">The request to send</param>
/// <param name="weight">The weight of the query</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request) protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request, int weight)
{ {
var dataResult = new CallResult<T>(new ServerError("No response on query received")); var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, data => await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, weight, data =>
{ {
if (!HandleQueryResponse<T>(socket, request, data, out var callResult)) if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false; return false;
@@ -518,8 +526,8 @@ namespace CryptoExchange.Net
} }
var subscription = request == null var subscription = request == null
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler) ? SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier!, userSubscription, authenticated, InternalHandler)
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler); : SocketSubscription.CreateForRequest(ExchangeHelpers.NextId(), request, userSubscription, authenticated, InternalHandler);
if (!connection.AddSubscription(subscription)) if (!connection.AddSubscription(subscription))
return null; return null;
return subscription; return subscription;
@@ -533,7 +541,7 @@ namespace CryptoExchange.Net
protected void AddGenericHandler(string identifier, Action<MessageEvent> action) protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
{ {
genericHandlers.Add(identifier, action); genericHandlers.Add(identifier, action);
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action); var subscription = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier, false, false, action);
foreach (var connection in socketConnections.Values) foreach (var connection in socketConnections.Values)
connection.AddSubscription(subscription); connection.AddSubscription(subscription);
} }
@@ -607,7 +615,7 @@ namespace CryptoExchange.Net
socketConnection.UnhandledMessage += HandleUnhandledMessage; socketConnection.UnhandledMessage += HandleUnhandledMessage;
foreach (var kvp in genericHandlers) foreach (var kvp in genericHandlers)
{ {
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value); var handler = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), kvp.Key, false, false, kvp.Value);
socketConnection.AddSubscription(handler); socketConnection.AddSubscription(handler);
} }
@@ -651,7 +659,7 @@ namespace CryptoExchange.Net
DataInterpreterString = dataInterpreterString, DataInterpreterString = dataInterpreterString,
KeepAliveInterval = KeepAliveInterval, KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval, ReconnectInterval = ClientOptions.ReconnectInterval,
RatelimitPerSecond = RateLimitPerSocketPerSecond, RateLimiters = RateLimiters,
Proxy = ClientOptions.Proxy, Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
}; };
@@ -704,7 +712,7 @@ namespace CryptoExchange.Net
try try
{ {
socketConnection.Send(obj); socketConnection.Send(ExchangeHelpers.NextId(), obj, 1);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -762,6 +770,10 @@ namespace CryptoExchange.Net
/// <returns></returns> /// <returns></returns>
public virtual async Task UnsubscribeAllAsync() public virtual async Task UnsubscribeAllAsync()
{ {
var sum = socketConnections.Sum(s => s.Value.SubscriptionCount);
if (sum == 0)
return;
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions"); _logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
+4 -4
View File
@@ -6,16 +6,16 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency API's</Description> <Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>6.0.0</PackageVersion> <PackageVersion>6.1.0</PackageVersion>
<AssemblyVersion>6.0.0</AssemblyVersion> <AssemblyVersion>6.1.0</AssemblyVersion>
<FileVersion>6.0.0</FileVersion> <FileVersion>6.1.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl> <RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>6.0.0 - Updated ApiCredentials to support RSA signing as well as the default Hmac signature, Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly, Refactored client options for easier use, Added easier way of switching environments, Added ResponseLength and ToString() override on WebCallResult object, Fixed memory leak in AsyncResetEvent</PackageReleaseNotes> <PackageReleaseNotes>6.1.0 - Added support for ratelimiting on socket connections, Added rest ratelimit handling and parsing, Added ServerRatelimitError error</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
+23
View File
@@ -8,6 +8,15 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public static class ExchangeHelpers public static class ExchangeHelpers
{ {
/// <summary>
/// The last used id, use NextId() to get the next id and up this
/// </summary>
private static int _lastId;
/// <summary>
/// Lock for id generating
/// </summary>
private static object _idLock = new();
/// <summary> /// <summary>
/// Clamp a value between a min and max /// Clamp a value between a min and max
/// </summary> /// </summary>
@@ -118,5 +127,19 @@ namespace CryptoExchange.Net
{ {
return value / 1.000000000000000000000000000000000m; return value / 1.000000000000000000000000000000000m;
} }
/// <summary>
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
/// </summary>
/// <returns></returns>
public static int NextId()
{
lock (_idLock)
{
_lastId += 1;
return _lastId;
}
}
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System; using System;
using System.Net.Http; using System.Net.Http;
@@ -23,6 +24,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
/// <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>
void Configure(TimeSpan requestTimeout, HttpClient? httpClient=null); /// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
} }
} }
+8 -2
View File
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
{ {
/// <summary> /// <summary>
/// Webscoket connection interface /// Websocket connection interface
/// </summary> /// </summary>
public interface IWebsocket: IDisposable public interface IWebsocket: IDisposable
{ {
@@ -21,6 +21,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
event Action<string> OnMessage; event Action<string> OnMessage;
/// <summary> /// <summary>
/// Websocket sent event, RequestId as parameter
/// </summary>
event Action<int> OnRequestSent;
/// <summary>
/// Websocket error event /// Websocket error event
/// </summary> /// </summary>
event Action<Exception> OnError; event Action<Exception> OnError;
@@ -69,8 +73,10 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Send data /// Send data
/// </summary> /// </summary>
/// <param name="id"></param>
/// <param name="data"></param> /// <param name="data"></param>
void Send(string data); /// <param name="weight"></param>
void Send(int id, string data, int weight);
/// <summary> /// <summary>
/// Reconnect the socket /// Reconnect the socket
/// </summary> /// </summary>
+129 -10
View File
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.Objects using System;
namespace CryptoExchange.Net.Objects
{ {
/// <summary> /// <summary>
/// Base class for errors /// Base class for errors
@@ -39,7 +41,7 @@
/// <returns></returns> /// <returns></returns>
public override string ToString() public override string ToString()
{ {
return $"{Code}: {Message} {Data}"; return Code != null ? $"{Code}: {Message} {Data}" : $"{Message} {Data}";
} }
} }
@@ -52,6 +54,14 @@
/// ctor /// ctor
/// </summary> /// </summary>
public CantConnectError() : base(null, "Can't connect to the server", null) { } public CantConnectError() : base(null, "Can't connect to the server", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -63,12 +73,20 @@
/// ctor /// ctor
/// </summary> /// </summary>
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { } public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
} }
/// <summary> /// <summary>
/// Error returned by the server /// Error returned by the server
/// </summary> /// </summary>
public class ServerError: Error public class ServerError : Error
{ {
/// <summary> /// <summary>
/// ctor /// ctor
@@ -83,9 +101,15 @@
/// <param name="code"></param> /// <param name="code"></param>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="data"></param> /// <param name="data"></param>
public ServerError(int code, string message, object? data = null) : base(code, message, data) public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
{
} /// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -107,6 +131,14 @@
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="data"></param> /// <param name="data"></param>
public WebError(int code, string message, object? data = null) : base(code, message, data) { } public WebError(int code, string message, object? data = null) : base(code, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected WebError(int? code, string message, object? data): base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -120,6 +152,14 @@
/// <param name="message">The error message</param> /// <param name="message">The error message</param>
/// <param name="data">The data which caused the error</param> /// <param name="data">The data which caused the error</param>
public DeserializeError(string message, object? data) : base(null, message, data) { } public DeserializeError(string message, object? data) : base(null, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -133,6 +173,14 @@
/// <param name="message">Error message</param> /// <param name="message">Error message</param>
/// <param name="data">Error data</param> /// <param name="data">Error data</param>
public UnknownError(string message, object? data = null) : base(null, message, data) { } public UnknownError(string message, object? data = null) : base(null, message, data) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -145,18 +193,73 @@
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { } public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
} }
/// <summary> /// <summary>
/// Rate limit exceeded /// Rate limit exceeded (client side)
/// </summary> /// </summary>
public class RateLimitError: Error public abstract class BaseRateLimitError : Error
{
/// <summary>
/// When the request can be retried
/// </summary>
public DateTime? RetryAfter { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded (client side)
/// </summary>
public class ClientRateLimitError : BaseRateLimitError
{ {
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { } public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>
/// Rate limit exceeded (server side)
/// </summary>
public class ServerRateLimitError : BaseRateLimitError
{
/// <summary>
/// ctor
/// </summary>
/// <param name="message"></param>
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -168,17 +271,33 @@
/// ctor /// ctor
/// </summary> /// </summary>
public CancellationRequestedError() : base(null, "Cancellation requested", null) { } public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
} }
/// <summary> /// <summary>
/// Invalid operation requested /// Invalid operation requested
/// </summary> /// </summary>
public class InvalidOperationError: Error public class InvalidOperationError : Error
{ {
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
public InvalidOperationError(string message) : base(null, message, null) { } public InvalidOperationError(string message) : base(null, message, null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
} }
} }
-329
View File
@@ -1,329 +0,0 @@
//using System;
//using System.Collections.Generic;
//using System.Linq;
//using System.Net.Http;
//using CryptoExchange.Net.Authentication;
//using CryptoExchange.Net.Interfaces;
//using CryptoExchange.Net.Logging;
//using Microsoft.Extensions.Logging;
//namespace CryptoExchange.Net.Objects
//{
// /// <summary>
// /// Client options
// /// </summary>
// public abstract class ClientOptions
// {
// internal event Action? OnLoggingChanged;
// private LogLevel _logLevel = LogLevel.Information;
// /// <summary>
// /// The minimum log level to output
// /// </summary>
// public LogLevel LogLevel
// {
// get => _logLevel;
// set
// {
// _logLevel = value;
// OnLoggingChanged?.Invoke();
// }
// }
// private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
// /// <summary>
// /// The log writers
// /// </summary>
// public List<ILogger> LogWriters
// {
// get => _logWriters;
// set
// {
// _logWriters = value;
// OnLoggingChanged?.Invoke();
// }
// }
// /// <summary>
// /// Proxy to use when connecting
// /// </summary>
// public ApiProxy? Proxy { get; set; }
// /// <summary>
// /// The api credentials used for signing requests to this API.
// /// </summary>
// public ApiCredentials? ApiCredentials { get; set; }
// /// <summary>
// /// ctor
// /// </summary>
// public ClientOptions()
// {
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="clientOptions">Copy values for the provided options</param>
// public ClientOptions(ClientOptions? clientOptions)
// {
// if (clientOptions == null)
// return;
// LogLevel = clientOptions.LogLevel;
// LogWriters = clientOptions.LogWriters.ToList();
// Proxy = clientOptions.Proxy;
// ApiCredentials = clientOptions.ApiCredentials?.Copy();
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseOptions">Copy values for the provided options</param>
// /// <param name="newValues">Copy values for the provided options</param>
// internal ClientOptions(ClientOptions baseOptions, ClientOptions? newValues)
// {
// Proxy = newValues?.Proxy ?? baseOptions.Proxy;
// LogLevel = baseOptions.LogLevel;
// LogWriters = baseOptions.LogWriters.ToList();
// ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
// }
// /// <inheritdoc />
// public override string ToString()
// {
// return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
// }
// }
// /// <summary>
// /// API client options
// /// </summary>
// public class ApiClientOptions
// {
// /// <summary>
// /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
// /// </summary>
// public bool OutputOriginalData { get; set; } = false;
// /// <summary>
// /// The base address of the API
// /// </summary>
// public string BaseAddress { get; set; }
// /// <summary>
// /// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
// /// </summary>
// public ApiCredentials? ApiCredentials { get; set; }
// /// <summary>
// /// ctor
// /// </summary>
//#pragma warning disable 8618 // Will always get filled by the implementation
// public ApiClientOptions()
// {
// }
//#pragma warning restore 8618
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseAddress">Base address for the API</param>
// public ApiClientOptions(string baseAddress)
// {
// BaseAddress = baseAddress;
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseOptions">Copy values for the provided options</param>
// /// <param name="newValues">Copy values for the provided options</param>
// public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
// {
// BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
// ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
// OutputOriginalData = newValues?.OutputOriginalData ?? baseOptions.OutputOriginalData;
// }
// /// <inheritdoc />
// public override string ToString()
// {
// return $"OutputOriginalData: {OutputOriginalData}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
// }
// }
// /// <summary>
// /// Rest API client options
// /// </summary>
// public class RestApiClientOptions: ApiClientOptions
// {
// /// <summary>
// /// The time the server has to respond to a request before timing out
// /// </summary>
// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
// /// <summary>
// /// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
// /// </summary>
// public HttpClient? HttpClient { get; set; }
// /// <summary>
// /// List of rate limiters to use
// /// </summary>
// public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
// /// <summary>
// /// What to do when a call would exceed the rate limit
// /// </summary>
// public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
// /// <summary>
// /// Whether or not to automatically sync the local time with the server time
// /// </summary>
// public bool AutoTimestamp { get; set; }
// /// <summary>
// /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
// /// </summary>
// public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
// /// <summary>
// /// ctor
// /// </summary>
// public RestApiClientOptions()
// {
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseAddress">Base address for the API</param>
// public RestApiClientOptions(string baseAddress): base(baseAddress)
// {
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseOn">Copy values for the provided options</param>
// /// <param name="newValues">Copy values for the provided options</param>
// public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
// {
// HttpClient = newValues?.HttpClient ?? baseOn.HttpClient;
// RequestTimeout = newValues == default ? baseOn.RequestTimeout : newValues.RequestTimeout;
// RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
// AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
// TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
// RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
// }
// /// <inheritdoc />
// public override string ToString()
// {
// return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
// }
// }
// /// <summary>
// /// Rest API client options
// /// </summary>
// public class SocketApiClientOptions : ApiClientOptions
// {
// /// <summary>
// /// Whether or not the socket should automatically reconnect when losing connection
// /// </summary>
// public bool AutoReconnect { get; set; } = true;
// /// <summary>
// /// Time to wait between reconnect attempts
// /// </summary>
// public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
// /// <summary>
// /// Max number of concurrent resubscription tasks per socket after reconnecting a socket
// /// </summary>
// public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
// /// <summary>
// /// The max time to wait for a response after sending a request on the socket before giving a timeout
// /// </summary>
// public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
// /// <summary>
// /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
// /// for example when the server sends intermittent ping requests
// /// </summary>
// public TimeSpan SocketNoDataTimeout { get; set; }
// /// <summary>
// /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
// /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
// /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
// /// </summary>
// public int? SocketSubscriptionsCombineTarget { get; set; }
// /// <summary>
// /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
// /// </summary>
// public int? MaxSocketConnections { get; set; }
// /// <summary>
// /// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
// /// </summary>
// public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
// /// <summary>
// /// ctor
// /// </summary>
// public SocketApiClientOptions()
// {
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseAddress">Base address for the API</param>
// public SocketApiClientOptions(string baseAddress) : base(baseAddress)
// {
// }
// /// <summary>
// /// ctor
// /// </summary>
// /// <param name="baseOptions">Copy values for the provided options</param>
// /// <param name="newValues">Copy values for the provided options</param>
// public SocketApiClientOptions(SocketApiClientOptions baseOptions, SocketApiClientOptions? newValues) : base(baseOptions, newValues)
// {
// if (baseOptions == null)
// return;
// AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
// ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
// MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
// SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
// SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
// SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
// MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
// DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
// }
// /// <inheritdoc />
// public override string ToString()
// {
// return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
// }
// }
// /// <summary>
// /// Base for order book options
// /// </summary>
// public class OrderBookOptions : ClientOptions
// {
// /// <summary>
// /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
// /// </summary>
// public bool ChecksumValidationEnabled { get; set; } = true;
// }
//}
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using System; using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects.Options namespace CryptoExchange.Net.Objects.Options
{ {
@@ -8,6 +10,11 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public class SocketApiOptions : ApiOptions public class SocketApiOptions : ApiOptions
{ {
/// <summary>
/// List of rate limiters to use
/// </summary>
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
/// <summary> /// <summary>
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected, /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
/// for example when the server sends intermittent ping requests /// for example when the server sends intermittent ping requests
@@ -30,6 +37,7 @@ namespace CryptoExchange.Net.Objects.Options
{ {
ApiCredentials = ApiCredentials?.Copy(), ApiCredentials = ApiCredentials?.Copy(),
OutputOriginalData = OutputOriginalData, OutputOriginalData = OutputOriginalData,
RateLimiters = RateLimiters,
SocketNoDataTimeout = SocketNoDataTimeout, SocketNoDataTimeout = SocketNoDataTimeout,
MaxSocketConnections = MaxSocketConnections, MaxSocketConnections = MaxSocketConnections,
}; };
+56 -20
View File
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects
public class RateLimiter : IRateLimiter public class RateLimiter : IRateLimiter
{ {
private readonly object _limiterLock = new object(); private readonly object _limiterLock = new object();
internal List<Limiter> Limiters = new List<Limiter>(); internal List<Limiter> _limiters = new List<Limiter>();
/// <summary> /// <summary>
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>, /// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
@@ -35,7 +35,7 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod) public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null)); _limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
return this; return this;
} }
@@ -50,7 +50,7 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false) public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits)); _limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this; return this;
} }
@@ -65,7 +65,7 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false) public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits)); _limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this; return this;
} }
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false) public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint)); _limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
return this; return this;
} }
@@ -95,7 +95,20 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit) public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit)); _limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
return this;
}
/// <summary>
/// Add a rate limit for the amount of messages that can be send per connection
/// </summary>
/// <param name="endpoint">The endpoint that the limit is for</param>
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
/// <param name="perTimePeriod">The time period the limit is for</param>
public RateLimiter AddConnectionRateLimit(string endpoint, int limit, TimeSpan perTimePeriod)
{
lock (_limiterLock)
_limiters.Add(new ConnectionRateLimiter(new[] { endpoint }, limit, perTimePeriod));
return this; return this;
} }
@@ -106,7 +119,7 @@ namespace CryptoExchange.Net.Objects
EndpointRateLimiter? endpointLimit; EndpointRateLimiter? endpointLimit;
lock (_limiterLock) lock (_limiterLock)
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)); endpointLimit = _limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
if(endpointLimit != null) if(endpointLimit != null)
{ {
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false); var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
@@ -121,7 +134,7 @@ namespace CryptoExchange.Net.Objects
List<PartialEndpointRateLimiter> partialEndpointLimits; List<PartialEndpointRateLimiter> partialEndpointLimits;
lock (_limiterLock) lock (_limiterLock)
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList(); partialEndpointLimits = _limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
foreach (var partialEndpointLimit in partialEndpointLimits) foreach (var partialEndpointLimit in partialEndpointLimits)
{ {
if (partialEndpointLimit.CountPerEndpoint) if (partialEndpointLimit.CountPerEndpoint)
@@ -129,11 +142,11 @@ namespace CryptoExchange.Net.Objects
SingleTopicRateLimiter? thisEndpointLimit; SingleTopicRateLimiter? thisEndpointLimit;
lock (_limiterLock) lock (_limiterLock)
{ {
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint); thisEndpointLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
if (thisEndpointLimit == null) if (thisEndpointLimit == null)
{ {
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit); thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
Limiters.Add(thisEndpointLimit); _limiters.Add(thisEndpointLimit);
} }
} }
@@ -158,7 +171,7 @@ namespace CryptoExchange.Net.Objects
ApiKeyRateLimiter? apiLimit; ApiKeyRateLimiter? apiLimit;
lock (_limiterLock) lock (_limiterLock)
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey); apiLimit = _limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
if (apiLimit != null) if (apiLimit != null)
{ {
if(apiKey == null) if(apiKey == null)
@@ -177,11 +190,11 @@ namespace CryptoExchange.Net.Objects
SingleTopicRateLimiter? thisApiLimit; SingleTopicRateLimiter? thisApiLimit;
lock (_limiterLock) lock (_limiterLock)
{ {
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey)); thisApiLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
if (thisApiLimit == null) if (thisApiLimit == null)
{ {
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit); thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
Limiters.Add(thisApiLimit); _limiters.Add(thisApiLimit);
} }
} }
@@ -198,7 +211,7 @@ namespace CryptoExchange.Net.Objects
TotalRateLimiter? totalLimit; TotalRateLimiter? totalLimit;
lock (_limiterLock) lock (_limiterLock)
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault(); totalLimit = _limiters.OfType<TotalRateLimiter>().SingleOrDefault();
if (totalLimit != null) if (totalLimit != null)
{ {
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false); var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
@@ -224,6 +237,8 @@ namespace CryptoExchange.Net.Objects
} }
sw.Stop(); sw.Stop();
try
{
int totalWaitTime = 0; int totalWaitTime = 0;
while (true) while (true)
{ {
@@ -240,7 +255,7 @@ namespace CryptoExchange.Net.Objects
break; break;
} }
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight); var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
if (currentWeight + requestWeight > historyTopic.Limit) if (currentWeight + requestWeight > historyTopic.Limit)
{ {
if (currentWeight == 0) if (currentWeight == 0)
@@ -248,18 +263,17 @@ namespace CryptoExchange.Net.Objects
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}"); $"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
// Wait until the next entry should be removed from the history // Wait until the next entry should be removed from the history
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds); var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
if (thisWaitTime > 0) if (thisWaitTime > 0)
{ {
if (limitBehaviour == RateLimitingBehaviour.Fail) if (limitBehaviour == RateLimitingBehaviour.Fail)
{ {
historyTopic.Semaphore.Release();
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}"; var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
logger.Log(LogLevel.Warning, msg); logger.Log(LogLevel.Warning, msg);
return new CallResult<int>(new RateLimitError(msg)); return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
} }
logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}"); logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
try try
{ {
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false); await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
@@ -279,9 +293,13 @@ namespace CryptoExchange.Net.Objects
var newTime = DateTime.UtcNow; var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight)); historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
historyTopic.Semaphore.Release();
return new CallResult<int>(totalWaitTime); return new CallResult<int>(totalWaitTime);
} }
finally
{
historyTopic.Semaphore.Release();
}
}
internal struct LimitEntry internal struct LimitEntry
{ {
@@ -329,6 +347,24 @@ namespace CryptoExchange.Net.Objects
} }
} }
internal class ConnectionRateLimiter : PartialEndpointRateLimiter
{
public ConnectionRateLimiter(int limit, TimeSpan perPeriod)
: base(new[] { "/" }, limit, perPeriod, null, true, true)
{
}
public ConnectionRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod)
: base(endpoints, limit, perPeriod, null, true, true)
{
}
public override string ToString()
{
return nameof(ConnectionRateLimiter);
}
}
internal class EndpointRateLimiter: Limiter internal class EndpointRateLimiter: Limiter
{ {
public string[] Endpoints { get; set; } public string[] Endpoints { get; set; }
+12 -1
View File
@@ -9,8 +9,19 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public class TraceLoggerProvider : ILoggerProvider public class TraceLoggerProvider : ILoggerProvider
{ {
private readonly LogLevel _logLevel;
/// <summary>
/// ctor
/// </summary>
/// <param name="logLevel"></param>
public TraceLoggerProvider(LogLevel? logLevel = null)
{
_logLevel = logLevel ?? LogLevel.Trace;
}
/// <inheritdoc /> /// <inheritdoc />
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName); public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() { } public void Dispose() { }
} }
+19 -2
View File
@@ -1,8 +1,10 @@
using System; using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Requests namespace CryptoExchange.Net.Requests
{ {
@@ -14,14 +16,29 @@ namespace CryptoExchange.Net.Requests
private HttpClient? _httpClient; private HttpClient? _httpClient;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(TimeSpan requestTimeout, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
_httpClient = client ?? new HttpClient() if (client == null)
{
var handler = new HttpClientHandler();
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 Timeout = requestTimeout
}; };
} }
_httpClient = client;
}
/// <inheritdoc /> /// <inheritdoc />
public IRequest Create(HttpMethod method, Uri uri, int requestId) public IRequest Create(HttpMethod method, Uri uri, int requestId)
{ {
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -30,9 +31,8 @@ namespace CryptoExchange.Net.Sockets
private static readonly object _streamIdLock = new(); private static readonly object _streamIdLock = new();
private readonly AsyncResetEvent _sendEvent; private readonly AsyncResetEvent _sendEvent;
private readonly ConcurrentQueue<byte[]> _sendBuffer; private readonly ConcurrentQueue<SendItem> _sendBuffer;
private readonly SemaphoreSlim _closeSem; private readonly SemaphoreSlim _closeSem;
private readonly List<DateTime> _outgoingMessages;
private ClientWebSocket _socket; private ClientWebSocket _socket;
private CancellationTokenSource _ctsSource; private CancellationTokenSource _ctsSource;
@@ -103,6 +103,9 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc /> /// <inheritdoc />
public event Action<string>? OnMessage; public event Action<string>? OnMessage;
/// <inheritdoc />
public event Action<int>? OnRequestSent;
/// <inheritdoc /> /// <inheritdoc />
public event Action<Exception>? OnError; public event Action<Exception>? OnError;
@@ -128,10 +131,9 @@ namespace CryptoExchange.Net.Sockets
_logger = logger; _logger = logger;
Parameters = websocketParameters; Parameters = websocketParameters;
_outgoingMessages = new List<DateTime>();
_receivedMessages = new List<ReceiveItem>(); _receivedMessages = new List<ReceiveItem>();
_sendEvent = new AsyncResetEvent(); _sendEvent = new AsyncResetEvent();
_sendBuffer = new ConcurrentQueue<byte[]>(); _sendBuffer = new ConcurrentQueue<SendItem>();
_ctsSource = new CancellationTokenSource(); _ctsSource = new CancellationTokenSource();
_receivedMessagesLock = new object(); _receivedMessagesLock = new object();
@@ -270,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual void Send(string data) public virtual void Send(int id, string data, int weight)
{ {
if (_ctsSource.IsCancellationRequested) if (_ctsSource.IsCancellationRequested)
return; return;
var bytes = Parameters.Encoding.GetBytes(data); var bytes = Parameters.Encoding.GetBytes(data);
_logger.Log(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer"); _logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} to send buffer");
_sendBuffer.Enqueue(bytes); _sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
_sendEvent.Set(); _sendEvent.Set();
} }
@@ -392,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
{ {
try try
{ {
var limitKey = Uri.ToString() + "/" + Id.ToString();
while (true) while (true)
{ {
if (_ctsSource.IsCancellationRequested) if (_ctsSource.IsCancellationRequested)
@@ -404,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
while (_sendBuffer.TryDequeue(out var data)) while (_sendBuffer.TryDequeue(out var data))
{ {
if (Parameters.RatelimitPerSecond != null) if (Parameters.RateLimiters != null)
{ {
// Wait for rate limit foreach(var ratelimiter in Parameters.RateLimiters)
DateTime? start = null;
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
{ {
start ??= DateTime.UtcNow; var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false); if (limitResult.Success)
{
if (limitResult.Data > 0)
_logger.Log(LogLevel.Debug, $"Socket {Id} - msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
}
} }
if (start != null)
_logger.Log(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
} }
try try
{ {
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false); await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
_outgoingMessages.Add(DateTime.UtcNow); OnRequestSent?.Invoke(data.Id);
_logger.Log(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes"); _logger.Log(LogLevel.Trace, $"Socket {Id} - msg {data.Id} - sent {data.Bytes.Length} bytes");
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -630,42 +632,6 @@ namespace CryptoExchange.Net.Sockets
} }
} }
/// <summary>
/// Trigger the OnMessage event
/// </summary>
/// <param name="data"></param>
protected void TriggerOnMessage(string data)
{
LastActionTime = DateTime.UtcNow;
OnMessage?.Invoke(data);
}
/// <summary>
/// Trigger the OnError event
/// </summary>
/// <param name="ex"></param>
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
/// <summary>
/// Trigger the OnError event
/// </summary>
protected void TriggerOnOpen() => OnOpen?.Invoke();
/// <summary>
/// Trigger the OnError event
/// </summary>
protected void TriggerOnClose() => OnClose?.Invoke();
/// <summary>
/// Trigger the OnReconnecting event
/// </summary>
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
/// <summary>
/// Trigger the OnReconnected event
/// </summary>
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
/// <summary> /// <summary>
/// Checks if there is no data received for a period longer than the specified timeout /// Checks if there is no data received for a period longer than the specified timeout
/// </summary> /// </summary>
@@ -721,13 +687,6 @@ namespace CryptoExchange.Net.Sockets
} }
} }
private int MessagesSentLastSecond()
{
var testTime = DateTime.UtcNow;
_outgoingMessages.RemoveAll(r => testTime - r > TimeSpan.FromSeconds(1));
return _outgoingMessages.Count;
}
/// <summary> /// <summary>
/// Update the received messages list, removing messages received longer than 3s ago /// Update the received messages list, removing messages received longer than 3s ago
/// </summary> /// </summary>
@@ -769,6 +728,32 @@ namespace CryptoExchange.Net.Sockets
} }
} }
/// <summary>
/// Message info
/// </summary>
public struct SendItem
{
/// <summary>
/// The request id
/// </summary>
public int Id { get; set; }
/// <summary>
/// The request id
/// </summary>
public int Weight { get; set; }
/// <summary>
/// Timestamp the request was sent
/// </summary>
public DateTime SendTime { get; set; }
/// <summary>
/// The bytes to send
/// </summary>
public byte[] Bytes { get; set; }
}
/// <summary> /// <summary>
/// Received message info /// Received message info
/// </summary> /// </summary>
+9 -3
View File
@@ -7,6 +7,7 @@ namespace CryptoExchange.Net.Sockets
{ {
internal class PendingRequest internal class PendingRequest
{ {
public int Id { get; set; }
public Func<JToken, bool> Handler { get; } public Func<JToken, bool> Handler { get; }
public JToken? Result { get; private set; } public JToken? Result { get; private set; }
public bool Completed { get; private set; } public bool Completed { get; private set; }
@@ -15,17 +16,22 @@ namespace CryptoExchange.Net.Sockets
public TimeSpan Timeout { get; } public TimeSpan Timeout { get; }
public SocketSubscription? Subscription { get; } public SocketSubscription? Subscription { get; }
private CancellationTokenSource _cts; private CancellationTokenSource? _cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription) public PendingRequest(int id, Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
{ {
Id = id;
Handler = handler; Handler = handler;
Event = new AsyncResetEvent(false, false); Event = new AsyncResetEvent(false, false);
Timeout = timeout; Timeout = timeout;
RequestTimestamp = DateTime.UtcNow; RequestTimestamp = DateTime.UtcNow;
Subscription = subscription; Subscription = subscription;
}
_cts = new CancellationTokenSource(timeout); public void IsSend()
{
// Start timeout countdown
_cts = new CancellationTokenSource(Timeout);
_cts.Token.Register(Fail, false); _cts.Token.Register(Fail, false);
} }
+61 -14
View File
@@ -108,6 +108,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public string Tag { get; set; } public string Tag { get; set; }
/// <summary>
/// Additional properties for this connection
/// </summary>
public Dictionary<string, object> Properties { get; set; }
/// <summary> /// <summary>
/// If activity is paused /// If activity is paused
/// </summary> /// </summary>
@@ -170,12 +175,14 @@ namespace CryptoExchange.Net.Sockets
_logger = logger; _logger = logger;
ApiClient = apiClient; ApiClient = apiClient;
Tag = tag; Tag = tag;
Properties = new Dictionary<string, object>();
_pendingRequests = new List<PendingRequest>(); _pendingRequests = new List<PendingRequest>();
_subscriptions = new List<SocketSubscription>(); _subscriptions = new List<SocketSubscription>();
_socket = socket; _socket = socket;
_socket.OnMessage += HandleMessage; _socket.OnMessage += HandleMessage;
_socket.OnRequestSent += HandleRequestSent;
_socket.OnOpen += HandleOpen; _socket.OnOpen += HandleOpen;
_socket.OnClose += HandleClose; _socket.OnClose += HandleClose;
_socket.OnReconnecting += HandleReconnecting; _socket.OnReconnecting += HandleReconnecting;
@@ -278,6 +285,22 @@ namespace CryptoExchange.Net.Sockets
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString()); _logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
} }
/// <summary>
/// Handler for whenever a request is sent over the websocket
/// </summary>
/// <param name="requestId">Id of the request sent</param>
protected virtual void HandleRequestSent(int requestId)
{
var pendingRequest = _pendingRequests.SingleOrDefault(p => p.Id == requestId);
if (pendingRequest == null)
{
_logger.Log(LogLevel.Debug, $"Socket {SocketId} - msg {requestId} - message sent, but not pending");
return;
}
pendingRequest.IsSend();
}
/// <summary> /// <summary>
/// Process a message received by the socket /// Process a message received by the socket
/// </summary> /// </summary>
@@ -312,7 +335,6 @@ namespace CryptoExchange.Net.Sockets
// Check if this message is an answer on any pending requests // Check if this message is an answer on any pending requests
foreach (var pendingRequest in requests) foreach (var pendingRequest in requests)
{ {
if (pendingRequest.CheckData(tokenData)) if (pendingRequest.CheckData(tokenData))
{ {
lock (_pendingRequests) lock (_pendingRequests)
@@ -323,12 +345,13 @@ namespace CryptoExchange.Net.Sockets
// Answer to a timed out request, unsub if it is a subscription request // Answer to a timed out request, unsub if it is a subscription request
if (pendingRequest.Subscription != null) if (pendingRequest.Subscription != null)
{ {
_logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout"); _logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the RequestTimeout");
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false); _ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
} }
} }
else else
{ {
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {pendingRequest.Id} - received data matched to pending request");
pendingRequest.Succeed(tokenData); pendingRequest.Succeed(tokenData);
} }
@@ -564,45 +587,69 @@ namespace CryptoExchange.Net.Sockets
/// <param name="timeout">The timeout for response</param> /// <param name="timeout">The timeout for response</param>
/// <param name="subscription">Subscription if this is a subscribe request</param> /// <param name="subscription">Subscription if this is a subscribe request</param>
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param> /// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
/// <param name="weight">The weight of the message</param>
/// <returns></returns> /// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, Func<JToken, bool> handler) public virtual async Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, int weight, Func<JToken, bool> handler)
{ {
var pending = new PendingRequest(handler, timeout, subscription); var pending = new PendingRequest(ExchangeHelpers.NextId(), handler, timeout, subscription);
lock (_pendingRequests) lock (_pendingRequests)
{ {
_pendingRequests.Add(pending); _pendingRequests.Add(pending);
} }
var sendOk = Send(obj);
if(!sendOk)
pending.Fail();
return pending.Event.WaitAsync(timeout); var sendOk = Send(pending.Id, obj, weight);
if (!sendOk)
{
pending.Fail();
return;
}
while (true)
{
if(!_socket.IsOpen)
{
pending.Fail();
return;
}
if (pending.Completed)
return;
await pending.Event.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
if (pending.Completed)
return;
}
} }
/// <summary> /// <summary>
/// Send data over the websocket connection /// Send data over the websocket connection
/// </summary> /// </summary>
/// <typeparam name="T">The type of the object to send</typeparam> /// <typeparam name="T">The type of the object to send</typeparam>
/// <param name="requestId">The request id</param>
/// <param name="obj">The object to send</param> /// <param name="obj">The object to send</param>
/// <param name="nullValueHandling">How null values should be serialized</param> /// <param name="nullValueHandling">How null values should be serialized</param>
public virtual bool Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore) /// <param name="weight">The weight of the message</param>
public virtual bool Send<T>(int requestId, T obj, int weight, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
{ {
if(obj is string str) if(obj is string str)
return Send(str); return Send(requestId, str, weight);
else else
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling })); return Send(requestId, JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }), weight);
} }
/// <summary> /// <summary>
/// Send string data over the websocket connection /// Send string data over the websocket connection
/// </summary> /// </summary>
/// <param name="data">The data to send</param> /// <param name="data">The data to send</param>
public virtual bool Send(string data) /// <param name="weight">The weight of the message</param>
/// <param name="requestId">The id of the request</param>
public virtual bool Send(int requestId, string data, int weight)
{ {
_logger.Log(LogLevel.Trace, $"Socket {SocketId} sending data: {data}"); _logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {requestId} - sending messsage: {data}");
try try
{ {
_socket.Send(data); _socket.Send(requestId, data, weight);
return true; return true;
} }
catch(Exception) catch(Exception)
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@@ -52,9 +53,9 @@ namespace CryptoExchange.Net.Sockets
public TimeSpan? KeepAliveInterval { get; set; } public TimeSpan? KeepAliveInterval { get; set; }
/// <summary> /// <summary>
/// The max amount of messages to send per second /// The rate limiters for the socket connection
/// </summary> /// </summary>
public int? RatelimitPerSecond { get; set; } public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary> /// <summary>
/// Origin header value to send in the connection handshake /// Origin header value to send in the connection handshake
+9 -14
View File
@@ -5,20 +5,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="8.0.6" /> <PackageReference Include="Binance.Net" Version="9.0.1" />
<PackageReference Include="Bitfinex.Net" Version="5.0.3" /> <PackageReference Include="Bitfinex.Net" Version="6.0.0" />
<PackageReference Include="Bittrex.Net" Version="7.0.4" /> <PackageReference Include="Bittrex.Net" Version="8.0.0" />
<PackageReference Include="Bybit.Net" Version="0.0.4" /> <PackageReference Include="Bybit.Net" Version="3.0.0" />
<PackageReference Include="CoinEx.Net" Version="5.0.3" /> <PackageReference Include="CoinEx.Net" Version="6.0.0" />
<PackageReference Include="FTX.Net" Version="1.0.4" /> <PackageReference Include="Huobi.Net" Version="5.0.0" />
<PackageReference Include="Huobi.Net" Version="4.0.4" /> <PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" /> <PackageReference Include="Kucoin.Net" Version="5.0.0" />
<PackageReference Include="Kucoin.Net" Version="4.0.3" /> <PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00250" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+11 -16
View File
@@ -1,13 +1,12 @@
@page "/" @page "/"
@inject IBinanceClient binanceClient @inject IBinanceRestClient binanceClient
@inject IBitfinexClient bitfinexClient @inject IBitfinexRestClient bitfinexClient
@inject IBittrexClient bittrexClient @inject IBittrexRestClient bittrexClient
@inject IBybitClient bybitClient @inject IBybitRestClient bybitClient
@inject ICoinExClient coinexClient @inject ICoinExRestClient coinexClient
@inject IFTXClient ftxClient @inject IHuobiRestClient huobiClient
@inject IHuobiClient huobiClient @inject IKrakenRestClient krakenClient
@inject IKrakenClient krakenClient @inject IKucoinRestClient kucoinClient
@inject IKucoinClient kucoinClient
<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))
@@ -23,14 +22,13 @@
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD"); var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var bybitTask = bybitClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var ftxTask = ftxClient.TradeApi.ExchangeData.GetSymbolAsync("BTC/USD");
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt"); var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD"); var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, ftxTask, huobiTask, krakenTask, kucoinTask); await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
if (binanceTask.Result.Success) if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice); _prices.Add("Binance", binanceTask.Result.Data.LastPrice);
@@ -42,14 +40,11 @@
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice); _prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
if (bybitTask.Result.Success) if (bybitTask.Result.Success)
_prices.Add("Bybit", bybitTask.Result.Data.LastPrice); _prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
if (coinexTask.Result.Success) if (coinexTask.Result.Success)
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice); _prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
if (ftxTask.Result.Success)
_prices.Add("FTX", ftxTask.Result.Data.LastPrice ?? 0);
if (huobiTask.Result.Success) if (huobiTask.Result.Success)
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0); _prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
+9 -19
View File
@@ -4,21 +4,12 @@
@inject IBittrexSocketClient bittrexSocketClient @inject IBittrexSocketClient bittrexSocketClient
@inject IBybitSocketClient bybitSocketClient @inject IBybitSocketClient bybitSocketClient
@inject ICoinExSocketClient coinExSocketClient @inject ICoinExSocketClient coinExSocketClient
@inject IFTXSocketClient ftxSocketClient
@inject IHuobiSocketClient huobiSocketClient @inject IHuobiSocketClient huobiSocketClient
@inject IKrakenSocketClient krakenSocketClient @inject IKrakenSocketClient krakenSocketClient
@inject IKucoinSocketClient kucoinSocketClient @inject IKucoinSocketClient kucoinSocketClient
@using Binance.Net.Clients.SpotApi @using System.Collections.Concurrent
@using Bitfinex.Net.Clients.SpotApi
@using Bittrex.Net.Clients.SpotApi
@using Bybit.Net.Clients.SpotApi
@using CoinEx.Net.Clients.SpotApi
@using CryptoExchange.Net.Objects @using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Sockets @using CryptoExchange.Net.Sockets
@using Huobi.Net.Clients.SpotApi
@using Kraken.Net.Clients.SpotApi
@using Kucoin.Net.Clients.SpotApi
@using System.Collections.Concurrent
@implements IDisposable @implements IDisposable
<h3>ETH-BTC prices, live updates:</h3> <h3>ETH-BTC prices, live updates:</h3>
@@ -35,15 +26,14 @@
{ {
var tasks = new Task<CallResult<UpdateSubscription>>[] var tasks = new Task<CallResult<UpdateSubscription>>[]
{ {
binanceSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)), binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
bitfinexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)), bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bittrexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)), bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
bybitSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)), bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)), coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
ftxSocketClient.Streams.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("FTX", data.Data.LastPrice ?? 0)), huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
huobiSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)), krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
krakenSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)), kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
}; };
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
+29 -25
View File
@@ -1,19 +1,24 @@
@page "/OrderBooks" @page "/OrderBooks"
@using Binance.Net.SymbolOrderBooks
@using Bitfinex.Net.SymbolOrderBooks
@using Bittrex.Net.SymbolOrderBooks
@using Bybit.Net.SymbolOrderBooks
@using CryptoExchange.Net.Interfaces
@using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Sockets
@using CoinEx.Net.SymbolOrderBooks
@using FTX.Net.SymbolOrderBooks
@using Huobi.Net.SymbolOrderBooks
@using Kraken.Net.SymbolOrderBooks
@using Kucoin.Net.Clients
@using Kucoin.Net.SymbolOrderBooks
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using System.Timers @using System.Timers
@using Binance.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bittrex.Net.Interfaces
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CryptoExchange.Net.Interfaces
@using Huobi.Net.Interfaces
@using Kraken.Net.Interfaces
@using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces
@inject IBinanceOrderBookFactory binanceFactory
@inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBittrexOrderBookFactory bittrexFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject IHuobiOrderBookFactory huobiFactory
@inject IKrakenOrderBookFactory krakenFactory
@inject IKucoinOrderBookFactory kucoinFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC books, live updates:</h3> <h3>ETH-BTC books, live updates:</h3>
@@ -40,22 +45,21 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand // Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
KucoinClient.SetDefaultOptions(new Kucoin.Net.Objects.KucoinClientOptions KucoinRestClient.SetDefaultOptions(options =>
{ {
ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE") options.ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE");
}); });
_books = new Dictionary<string, ISymbolOrderBook> _books = new Dictionary<string, ISymbolOrderBook>
{ {
{ "Binance", new BinanceSpotSymbolOrderBook("ETHBTC") }, { "Binance", binanceFactory.CreateSpot("ETHBTC") },
{ "Bitfinex", new BitfinexSymbolOrderBook("tETHBTC") }, { "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bittrex", new BittrexSymbolOrderBook("ETH-BTC") }, { "Bittrex", bittrexFactory.Create("ETH-BTC") },
{ "Bybit", new BybitSpotSymbolOrderBook("ETHBTC") }, { "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "CoinEx", new CoinExSpotSymbolOrderBook("ETHBTC") }, { "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "FTX", new FTXSymbolOrderBook("ETH/BTC") }, { "Huobi", huobiFactory.CreateSpot("ethbtc") },
{ "Huobi", new HuobiSpotSymbolOrderBook("ethbtc") }, { "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
{ "Kraken", new KrakenSpotSymbolOrderBook("ETH/XBT") }, { "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Kucoin", new KucoinSpotSymbolOrderBook("ETH-BTC") },
}; };
await Task.WhenAll(_books.Select(b => b.Value.StartAsync())); await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
@@ -70,7 +74,7 @@
{ {
_timer.Stop(); _timer.Stop();
_timer.Dispose(); _timer.Dispose();
foreach (var book in _books) foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
// It's not necessary to wait for this // It's not necessary to wait for this
_ = book.Value.StopAsync(); _ = book.Value.StopAsync();
} }
+9 -12
View File
@@ -1,13 +1,12 @@
@page "/SpotClient" @page "/SpotClient"
@inject IBinanceClient binanceClient @inject IBinanceRestClient binanceClient
@inject IBitfinexClient bitfinexClient @inject IBitfinexRestClient bitfinexClient
@inject IBittrexClient bittrexClient @inject IBittrexRestClient bittrexClient
@inject IBybitClient bybitClient @inject IBybitRestClient bybitClient
@inject ICoinExClient coinexClient @inject ICoinExRestClient coinexClient
@inject IFTXClient ftxClient @inject IHuobiRestClient huobiClient
@inject IHuobiClient huobiClient @inject IKrakenRestClient krakenClient
@inject IKrakenClient krakenClient @inject IKucoinRestClient kucoinClient
@inject IKucoinClient kucoinClient
@using Binance.Net.Clients.SpotApi @using Binance.Net.Clients.SpotApi
@using Bitfinex.Net.Clients.SpotApi @using Bitfinex.Net.Clients.SpotApi
@using Bittrex.Net.Clients.SpotApi @using Bittrex.Net.Clients.SpotApi
@@ -15,7 +14,6 @@
@using CoinEx.Net.Clients.SpotApi @using CoinEx.Net.Clients.SpotApi
@using CryptoExchange.Net.Interfaces @using CryptoExchange.Net.Interfaces
@using CryptoExchange.Net.Interfaces.CommonClients @using CryptoExchange.Net.Interfaces.CommonClients
@using FTX.Net.Clients.TradeApi
@using Huobi.Net.Clients.SpotApi @using Huobi.Net.Clients.SpotApi
@using Kraken.Net.Clients.SpotApi @using Kraken.Net.Clients.SpotApi
@using Kucoin.Net.Clients.SpotApi @using Kucoin.Net.Clients.SpotApi
@@ -37,9 +35,8 @@
binanceClient.SpotApi.CommonSpotClient, binanceClient.SpotApi.CommonSpotClient,
bitfinexClient.SpotApi.CommonSpotClient, bitfinexClient.SpotApi.CommonSpotClient,
bittrexClient.SpotApi.CommonSpotClient, bittrexClient.SpotApi.CommonSpotClient,
bybitClient.SpotApi.CommonSpotClient, bybitClient.SpotApiV1.CommonSpotClient,
coinexClient.SpotApi.CommonSpotClient, coinexClient.SpotApi.CommonSpotClient,
ftxClient.TradeApi.CommonSpotClient,
huobiClient.SpotApi.CommonSpotClient, huobiClient.SpotApi.CommonSpotClient,
krakenClient.SpotApi.CommonSpotClient, krakenClient.SpotApi.CommonSpotClient,
kucoinClient.SpotApi.CommonSpotClient kucoinClient.SpotApi.CommonSpotClient
+4 -25
View File
@@ -2,15 +2,11 @@ using System.Collections.Generic;
using Binance.Net; using Binance.Net;
using Binance.Net.Clients; using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients; using Binance.Net.Interfaces.Clients;
using Binance.Net.Objects;
using Bitfinex.Net; using Bitfinex.Net;
using Bittrex.Net; using Bittrex.Net;
using Bybit.Net; using Bybit.Net;
using CoinEx.Net; using CoinEx.Net;
using CoinEx.Net.Clients;
using CoinEx.Net.Interfaces.Clients;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using FTX.Net;
using Huobi.Net; using Huobi.Net;
using Kraken.Net; using Kraken.Net;
using Kucoin.Net; using Kucoin.Net;
@@ -43,35 +39,18 @@ namespace BlazorClient
services.AddServerSideBlazor(); services.AddServerSideBlazor();
// Register the clients, options can be provided in the callback parameter // Register the clients, options can be provided in the callback parameter
services.AddBinance((restClientOptions, socketClientOptions) => { services.AddBinance(restOptions =>
restClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
restClientOptions.LogLevel = LogLevel.Trace;
// Point the logging to use the ILogger configuration, which uses Serilog here
restClientOptions.LogWriters = new List<ILogger> { _loggerFactory.CreateLogger<IBinanceClient>() };
socketClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
});
BinanceClient.SetDefaultOptions(new BinanceClientOptions
{ {
ApiCredentials = new ApiCredentials("KEY", "SECRET"), restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
LogLevel = LogLevel.Trace }, socketOptions =>
});
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions
{ {
ApiCredentials = new ApiCredentials("KEY", "SECRET"), socketOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
}); });
services.AddTransient<IBinanceClient, BinanceClient>();
services.AddScoped<IBinanceSocketClient, BinanceSocketClient>();
services.AddBitfinex(); services.AddBitfinex();
services.AddBittrex(); services.AddBittrex();
services.AddBybit(); services.AddBybit();
services.AddCoinEx(); services.AddCoinEx();
services.AddFTX();
services.AddHuobi(); services.AddHuobi();
services.AddKraken(); services.AddKraken();
services.AddKucoin(); services.AddKucoin();
-1
View File
@@ -13,7 +13,6 @@
@using Bittrex.Net.Interfaces.Clients; @using Bittrex.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients; @using Bybit.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients; @using CoinEx.Net.Interfaces.Clients;
@using FTX.Net.Interfaces.Clients;
@using Huobi.Net.Interfaces.Clients; @using Huobi.Net.Interfaces.Clients;
@using Kraken.Net.Interfaces.Clients; @using Kraken.Net.Interfaces.Clients;
@using Kucoin.Net.Interfaces.Clients; @using Kucoin.Net.Interfaces.Clients;
+8 -9
View File
@@ -6,15 +6,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="8.0.6" /> <PackageReference Include="Binance.Net" Version="9.0.1" />
<PackageReference Include="Bitfinex.Net" Version="5.0.3" /> <PackageReference Include="Bitfinex.Net" Version="6.0.0" />
<PackageReference Include="Bittrex.Net" Version="7.0.4" /> <PackageReference Include="Bittrex.Net" Version="8.0.0" />
<PackageReference Include="Bybit.Net" Version="0.0.4" /> <PackageReference Include="Bybit.Net" Version="3.0.0" />
<PackageReference Include="CoinEx.Net" Version="5.0.3" /> <PackageReference Include="CoinEx.Net" Version="6.0.0" />
<PackageReference Include="FTX.Net" Version="1.0.4" /> <PackageReference Include="Huobi.Net" Version="5.0.0" />
<PackageReference Include="Huobi.Net" Version="4.0.4" /> <PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" /> <PackageReference Include="Kucoin.Net" Version="5.0.0" />
<PackageReference Include="Kucoin.Net" Version="4.0.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -17,21 +17,21 @@ namespace ConsoleClient.Exchanges
public async Task<WebCallResult> CancelOrder(string symbol, string id) public async Task<WebCallResult> CancelOrder(string symbol, string id)
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id)); var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id));
return result.AsDataless(); return result.AsDataless();
} }
public async Task<Dictionary<string, decimal>> GetBalances() public async Task<Dictionary<string, decimal>> GetBalances()
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Account.GetAccountInfoAsync(); var result = await client.SpotApi.Account.GetAccountInfoAsync();
return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total); return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total);
} }
public async Task<IEnumerable<OpenOrder>> GetOpenOrders() public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Trading.GetOpenOrdersAsync(); var result = await client.SpotApi.Trading.GetOpenOrdersAsync();
// Should check result success status here // Should check result success status here
return result.Data.Select(o => new OpenOrder return result.Data.Select(o => new OpenOrder
@@ -49,7 +49,7 @@ namespace ConsoleClient.Exchanges
public async Task<decimal> GetPrice(string symbol) public async Task<decimal> GetPrice(string symbol)
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol); var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
// Should check result success status here // Should check result success status here
return result.Data.Price; return result.Data.Price;
@@ -57,7 +57,7 @@ namespace ConsoleClient.Exchanges
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price) public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Trading.PlaceOrderAsync( var result = await client.SpotApi.Trading.PlaceOrderAsync(
symbol, symbol,
side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell, side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell,
@@ -70,7 +70,7 @@ namespace ConsoleClient.Exchanges
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler) public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
{ {
var sub = await _socketClient.SpotStreams.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice)); var sub = await _socketClient.SpotApi.ExchangeData.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
return sub.Data; return sub.Data;
} }
} }
@@ -0,0 +1,75 @@
using Bybit.Net.Clients;
using Bybit.Net.Interfaces.Clients;
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClient.Exchanges
{
internal class BybitExchange : IExchange
{
private IBybitSocketClient _socketClient = new BybitSocketClient();
public async Task<WebCallResult> CancelOrder(string symbol, string id)
{
using var client = new BybitRestClient();
var result = await client.V5Api.Trading.CancelOrderAsync(Bybit.Net.Enums.Category.Spot, symbol, id);
return result.AsDataless();
}
public async Task<Dictionary<string, decimal>> GetBalances()
{
using var client = new BybitRestClient();
var result = await client.V5Api.Account.GetBalancesAsync(Bybit.Net.Enums.AccountType.Spot);
return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance);
}
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
{
using var client = new BybitRestClient();
var order = await client.V5Api.Trading.GetOrdersAsync(Bybit.Net.Enums.Category.Spot);
return order.Data.List.Select(o => new OpenOrder
{
Symbol = o.Symbol,
OrderSide = o.Side.ToString(),
OrderStatus = o.Status.ToString(),
OrderTime = o.CreateTime,
OrderType = o.OrderType.ToString(),
Price = o.Price ?? 0,
Quantity = o.Quantity,
QuantityFilled = o.QuantityFilled ?? 0
});
}
public async Task<decimal> GetPrice(string symbol)
{
using var client = new BybitRestClient();
var result = await client.V5Api.ExchangeData.GetSpotTickersAsync(symbol);
return result.Data.List.First().LastPrice;
}
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
{
using var client = new BybitRestClient();
var result = await client.V5Api.Trading.PlaceOrderAsync(
Bybit.Net.Enums.Category.Spot,
symbol,
side.ToLower() == "buy" ? Bybit.Net.Enums.OrderSide.Buy : Bybit.Net.Enums.OrderSide.Sell,
type == "market" ? Bybit.Net.Enums.NewOrderType.Market : Bybit.Net.Enums.NewOrderType.Limit,
quantity,
price: price);
return result.As(result.Data?.OrderId.ToString());
}
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
{
var sub = await _socketClient.V5SpotApi.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
return sub.Data;
}
}
}
@@ -1,74 +0,0 @@
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using FTX.Net.Clients;
using FTX.Net.Interfaces.Clients;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClient.Exchanges
{
internal class FTXExchange : IExchange
{
private IFTXSocketClient _socketClient = new FTXSocketClient();
public async Task<WebCallResult> CancelOrder(string symbol, string id)
{
using var client = new FTXClient();
var result = await client.TradeApi.Trading.CancelOrderAsync(long.Parse(id));
return result.AsDataless();
}
public async Task<Dictionary<string, decimal>> GetBalances()
{
using var client = new FTXClient();
var result = await client.TradeApi.Account.GetBalancesAsync();
return result.Data.ToDictionary(d => d.Asset, d => d.Total);
}
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
{
using var client = new FTXClient();
var order = await client.TradeApi.Trading.GetOpenOrdersAsync();
return order.Data.Select(o => new OpenOrder
{
Symbol = o.Symbol,
OrderSide = o.Side.ToString(),
OrderStatus = o.Status.ToString(),
OrderTime = o.CreateTime,
OrderType = o.Type.ToString(),
Price = o.Price ?? 0,
Quantity = o.Quantity,
QuantityFilled = o.QuantityFilled ?? 0
});
}
public async Task<decimal> GetPrice(string symbol)
{
using var client = new FTXClient();
var result = await client.TradeApi.ExchangeData.GetSymbolAsync(symbol);
return result.Data.LastPrice ?? 0;
}
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
{
using var client = new FTXClient();
var result = await client.TradeApi.Trading.PlaceOrderAsync(
symbol,
side.ToLower() == "buy" ? FTX.Net.Enums.OrderSide.Buy : FTX.Net.Enums.OrderSide.Sell,
type == "market" ? FTX.Net.Enums.OrderType.Market : FTX.Net.Enums.OrderType.Limit,
quantity,
price: price);
return result.As(result.Data?.Id.ToString());
}
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
{
var sub = await _socketClient.Streams.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice ?? 0));
return sub.Data;
}
}
}
+6 -10
View File
@@ -5,12 +5,10 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Binance.Net.Clients; using Binance.Net.Clients;
using Binance.Net.Objects; using Binance.Net.Objects;
using Bybit.Net.Clients;
using ConsoleClient.Exchanges; using ConsoleClient.Exchanges;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using FTX.Net.Clients;
using FTX.Net.Objects;
using Microsoft.Extensions.Logging;
namespace ConsoleClient namespace ConsoleClient
{ {
@@ -19,20 +17,18 @@ namespace ConsoleClient
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange> static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
{ {
{ "Binance", new BinanceExchange() }, { "Binance", new BinanceExchange() },
{ "FTX", new FTXExchange() } { "Bybit", new BybitExchange() }
}; };
static async Task Main(string[] args) static async Task Main(string[] args)
{ {
BinanceClient.SetDefaultOptions(new BinanceClientOptions BinanceRestClient.SetDefaultOptions(options =>
{ {
LogLevel = LogLevel.Trace, options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
}); });
FTXClient.SetDefaultOptions(new FTXClientOptions BybitRestClient.SetDefaultOptions(options =>
{ {
LogLevel = LogLevel.Trace, options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
}); });
while (true) while (true)
+16 -3
View File
@@ -25,14 +25,27 @@ Use one of the following following referral links to signup to a new exchange to
### 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.
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS **Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2 **Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
### Sponsor ### Sponsor
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 6.1.0 - 24 Aug 2023
* Added support for ratelimiting on socket connections
* Added rest ratelimit handling and parsing
* Added ServerRatelimitError error
* Version 6.0.3 - 23 Jul 2023
* Fixed Proxy not getting applied in rest clients when not using DI
* Version 6.0.2 - 05 Jul 2023
* Added properties generic dictionary to SocketConnection
* Version 6.0.1 - 29 Jun 2023
* Added LogLevel optional parameter to TraceLoggerProvider
* Version 6.0.0 - 25 Jun 2023 * Version 6.0.0 - 25 Jun 2023
* Updated ApiCredentials to support RSA signing as well as the default Hmac signature * Updated ApiCredentials to support RSA signing as well as the default Hmac signature
* Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly * Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly
+4
View File
@@ -14,6 +14,8 @@ BinanceClient.SetDefaultOptions(options =>
{ {
options.OutputOriginalData = true; options.OutputOriginalData = true;
options.ApiCredentials = new ApiCredentials("KEY", "SECRET"); options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
// Override the api credentials for the Spot API
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
}); });
``` ```
@@ -25,6 +27,8 @@ var client = new BinanceClient(options =>
{ {
options.OutputOriginalData = true; options.OutputOriginalData = true;
options.ApiCredentials = new ApiCredentials("KEY", "SECRET"); options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
// Override the api credentials for the Spot API
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
}); });
``` ```
+1 -1
View File
@@ -20,6 +20,7 @@ These will always be on the latest CryptoExchange.Net version and the latest ver
|<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/| |<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/|
|<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/| |<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/|
|<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/| |<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/|
|<a href="https://github.com/JKorf/OKX.Net"><img src="https://raw.githubusercontent.com/JKorf/OKX.Net/358d31f58d8ee51fc234bff1940878a8d0ce5676/Okex.Net/Icon/icon.png"></a>|OKX|https://jkorf.github.io/OKX.Net/|
**Implementations by third parties** **Implementations by third parties**
These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version. These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version.
@@ -31,7 +32,6 @@ These might not be compatible with other libraries, make sure to check the Crypt
|<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex| |<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex|
|<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC| |<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC|
|<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin| |<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin|
|<a href="https://github.com/burakoner/OKEx.Net"><img src="https://github.com/burakoner/OKEx.Net/blob/master/Okex.Net/Icon/icon.png?raw=true"></a>|OKEx|
|<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz| |<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz|
|<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk| |<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk|
|<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex| |<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex|