mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a16b19019f | |||
| 2443f576ac | |||
| 4fd7e44015 | |||
| a0a3bda1c5 | |||
| 6bda7a3c73 | |||
| 69a7a714cd | |||
| 48e2e6468e | |||
| 4017ac780f | |||
| a55cd1bb13 | |||
| b34129e148 | |||
| be25a68c9c | |||
| 468cd5e48e | |||
| 262c4e4aa5 | |||
| 4ccff6461f | |||
| 3bfa3ef389 | |||
| 5238971bcc | |||
| 2f5c904faf | |||
| c62775813f | |||
| f11b3754f0 | |||
| 3cbe0465e9 | |||
| 5048aea722 | |||
| 8d35339ab2 | |||
| c3316a51e7 | |||
| 7ecf37064b | |||
| 18954f4f53 | |||
| 00bc245102 | |||
| 273cab9fdb |
@@ -140,6 +140,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||
client.SubClient.ConnectSocketSub(sub1);
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public event Action OnReconnected;
|
||||
public event Action OnReconnecting;
|
||||
#pragma warning restore 0067
|
||||
public event Action<int> OnRequestSent;
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
@@ -69,10 +70,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
return Task.FromResult(CanConnect);
|
||||
}
|
||||
|
||||
public void Send(string data)
|
||||
public void Send(int requestId, string data, int weight)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
|
||||
@@ -77,15 +77,6 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
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>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
@@ -338,19 +329,6 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,6 +15,7 @@ using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static CryptoExchange.Net.Objects.RateLimiter;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -71,7 +73,7 @@ namespace CryptoExchange.Net
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
|
||||
RequestFactory.Configure(options.RequestTimeout, httpClient);
|
||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -194,7 +196,7 @@ namespace CryptoExchange.Net
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
var requestId = NextId();
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
|
||||
if (signed)
|
||||
{
|
||||
@@ -344,8 +346,13 @@ namespace CryptoExchange.Net
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||
responseStream.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)
|
||||
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);
|
||||
@@ -529,13 +536,39 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
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() != true)
|
||||
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>
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static CryptoExchange.Net.Objects.RateLimiter;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -76,9 +77,9 @@ namespace CryptoExchange.Net
|
||||
protected internal bool UnhandledMessageExpected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of outgoing messages per socket per second
|
||||
/// The rate limiters
|
||||
/// </summary>
|
||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
||||
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
@@ -130,6 +131,10 @@ namespace CryptoExchange.Net
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -275,7 +280,7 @@ namespace CryptoExchange.Net
|
||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -295,10 +300,11 @@ namespace CryptoExchange.Net
|
||||
/// <typeparam name="T">Expected result type</typeparam>
|
||||
/// <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="weight">Weight of the request</param>
|
||||
/// <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>
|
||||
@@ -308,8 +314,9 @@ namespace CryptoExchange.Net
|
||||
/// <param name="url">The url for the request</param>
|
||||
/// <param name="request">The request to send</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <param name="weight">Weight of the request</param>
|
||||
/// <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)
|
||||
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 await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
||||
return await QueryAndWaitAsync<T>(socketConnection, request, weight).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -357,11 +364,12 @@ namespace CryptoExchange.Net
|
||||
/// <typeparam name="T">The expected result type</typeparam>
|
||||
/// <param name="socket">The connection to send and wait on</param>
|
||||
/// <param name="request">The request to send</param>
|
||||
/// <param name="weight">The weight of the query</param>
|
||||
/// <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"));
|
||||
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))
|
||||
return false;
|
||||
@@ -518,8 +526,8 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
var subscription = request == null
|
||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||
? SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||
: SocketSubscription.CreateForRequest(ExchangeHelpers.NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||
if (!connection.AddSubscription(subscription))
|
||||
return null;
|
||||
return subscription;
|
||||
@@ -533,7 +541,7 @@ namespace CryptoExchange.Net
|
||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> 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)
|
||||
connection.AddSubscription(subscription);
|
||||
}
|
||||
@@ -607,7 +615,7 @@ namespace CryptoExchange.Net
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -651,7 +659,7 @@ namespace CryptoExchange.Net
|
||||
DataInterpreterString = dataInterpreterString,
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||
RateLimiters = RateLimiters,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||
};
|
||||
@@ -704,7 +712,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
try
|
||||
{
|
||||
socketConnection.Send(obj);
|
||||
socketConnection.Send(ExchangeHelpers.NextId(), obj, 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -762,6 +770,10 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
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");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
|
||||
@@ -109,13 +109,21 @@ namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
if (token.Type == JTokenType.Null)
|
||||
value = null;
|
||||
|
||||
if (token.Type == JTokenType.Float)
|
||||
value = token.Value<decimal>();
|
||||
}
|
||||
|
||||
if ((property.PropertyType == typeof(decimal)
|
||||
if (value is decimal)
|
||||
{
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
var v = value.ToString();
|
||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
property.SetValue(result, dec);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -117,22 +117,32 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString<T>(T enumValue)
|
||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
private static string? GetString(Type objectType, object? enumValue)
|
||||
{
|
||||
var objectType = typeof(T);
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = GetString(value);
|
||||
writer.WriteValue(stringValue);
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteValue(stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>6.0.0</PackageVersion>
|
||||
<AssemblyVersion>6.0.0</AssemblyVersion>
|
||||
<FileVersion>6.0.0</FileVersion>
|
||||
<PackageVersion>6.1.3</PackageVersion>
|
||||
<AssemblyVersion>6.1.3</AssemblyVersion>
|
||||
<FileVersion>6.1.3</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<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.3 - Fix for concurrency exception in socket subscription</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
||||
@@ -8,6 +8,15 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
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>
|
||||
/// Clamp a value between a min and max
|
||||
/// </summary>
|
||||
@@ -118,5 +127,19 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -23,6 +24,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="requestTimeout">Request timeout to use</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,7 +8,7 @@ using System.Threading.Tasks;
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Webscoket connection interface
|
||||
/// Websocket connection interface
|
||||
/// </summary>
|
||||
public interface IWebsocket: IDisposable
|
||||
{
|
||||
@@ -21,6 +21,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
event Action<string> OnMessage;
|
||||
/// <summary>
|
||||
/// Websocket sent event, RequestId as parameter
|
||||
/// </summary>
|
||||
event Action<int> OnRequestSent;
|
||||
/// <summary>
|
||||
/// Websocket error event
|
||||
/// </summary>
|
||||
event Action<Exception> OnError;
|
||||
@@ -69,8 +73,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
void Send(string data);
|
||||
/// <param name="weight"></param>
|
||||
void Send(int id, string data, int weight);
|
||||
/// <summary>
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace CryptoExchange.Net.Objects
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for errors
|
||||
@@ -39,7 +41,7 @@
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Code}: {Message} {Data}";
|
||||
return Code != null ? $"{Code}: {Message} {Data}" : $"{Message} {Data}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +54,14 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -63,12 +73,20 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
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>
|
||||
/// Error returned by the server
|
||||
/// </summary>
|
||||
public class ServerError: Error
|
||||
public class ServerError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -83,9 +101,15 @@
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></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>
|
||||
@@ -107,6 +131,14 @@
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
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>
|
||||
@@ -120,6 +152,14 @@
|
||||
/// <param name="message">The error message</param>
|
||||
/// <param name="data">The data which caused the error</param>
|
||||
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>
|
||||
@@ -133,6 +173,14 @@
|
||||
/// <param name="message">Error message</param>
|
||||
/// <param name="data">Error data</param>
|
||||
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>
|
||||
@@ -145,18 +193,73 @@
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
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>
|
||||
/// Rate limit exceeded
|
||||
/// Rate limit exceeded (client side)
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <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>
|
||||
@@ -168,17 +271,33 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
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>
|
||||
/// Invalid operation requested
|
||||
/// </summary>
|
||||
public class InvalidOperationError: Error
|
||||
public class InvalidOperationError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
@@ -8,6 +10,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public class SocketApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <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
|
||||
@@ -30,6 +37,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
RateLimiters = RateLimiters,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public class RateLimiter : IRateLimiter
|
||||
{
|
||||
private readonly object _limiterLock = new object();
|
||||
internal List<Limiter> Limiters = new List<Limiter>();
|
||||
internal List<Limiter> _limiters = new List<Limiter>();
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||
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)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
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)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,20 @@ namespace CryptoExchange.Net.Objects
|
||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -104,10 +117,10 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
int totalWaitTime = 0;
|
||||
|
||||
EndpointRateLimiter? endpointLimit;
|
||||
List<EndpointRateLimiter> endpointLimits;
|
||||
lock (_limiterLock)
|
||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||
if(endpointLimit != null)
|
||||
endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
|
||||
foreach (var endpointLimit in endpointLimits)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
@@ -116,12 +129,12 @@ namespace CryptoExchange.Net.Objects
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
if (endpointLimit?.IgnoreOtherRateLimits == true)
|
||||
if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||
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)
|
||||
{
|
||||
if (partialEndpointLimit.CountPerEndpoint)
|
||||
@@ -129,11 +142,11 @@ namespace CryptoExchange.Net.Objects
|
||||
SingleTopicRateLimiter? thisEndpointLimit;
|
||||
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)
|
||||
{
|
||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
||||
Limiters.Add(thisEndpointLimit);
|
||||
_limiters.Add(thisEndpointLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +169,10 @@ namespace CryptoExchange.Net.Objects
|
||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
ApiKeyRateLimiter? apiLimit;
|
||||
List<ApiKeyRateLimiter> apiLimits;
|
||||
lock (_limiterLock)
|
||||
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
||||
if (apiLimit != null)
|
||||
apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
|
||||
foreach (var apiLimit in apiLimits)
|
||||
{
|
||||
if(apiKey == null)
|
||||
{
|
||||
@@ -177,11 +190,11 @@ namespace CryptoExchange.Net.Objects
|
||||
SingleTopicRateLimiter? thisApiLimit;
|
||||
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)
|
||||
{
|
||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
||||
Limiters.Add(thisApiLimit);
|
||||
_limiters.Add(thisApiLimit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,13 +206,13 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
|
||||
if ((signed || apiLimits.All(l => !l.OnlyForSignedRequests)) && apiLimits.Any(l => l.IgnoreTotalRateLimit))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
TotalRateLimiter? totalLimit;
|
||||
List<TotalRateLimiter> totalLimits;
|
||||
lock (_limiterLock)
|
||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||
if (totalLimit != null)
|
||||
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
|
||||
foreach(var totalLimit in totalLimits)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
@@ -224,63 +237,68 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
int totalWaitTime = 0;
|
||||
while (true)
|
||||
try
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||
int totalWaitTime = 0;
|
||||
while (true)
|
||||
{
|
||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||
{
|
||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||
i--;
|
||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||
{
|
||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||
i--;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
|
||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||
{
|
||||
if (currentWeight == 0)
|
||||
throw new Exception("Request limit reached without any prior request. " +
|
||||
$"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
|
||||
var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
|
||||
if (thisWaitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
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);
|
||||
return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
|
||||
}
|
||||
|
||||
logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
totalWaitTime += thisWaitTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
|
||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||
{
|
||||
if (currentWeight == 0)
|
||||
throw new Exception("Request limit reached without any prior request. " +
|
||||
$"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
|
||||
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
|
||||
if (thisWaitTime > 0)
|
||||
{
|
||||
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}";
|
||||
logger.Log(LogLevel.Warning, msg);
|
||||
return new CallResult<int>(new RateLimitError(msg));
|
||||
}
|
||||
|
||||
logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
totalWaitTime += thisWaitTime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var newTime = DateTime.UtcNow;
|
||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||
historyTopic.Semaphore.Release();
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
var newTime = DateTime.UtcNow;
|
||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
finally
|
||||
{
|
||||
historyTopic.Semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
public string[] Endpoints { get; set; }
|
||||
|
||||
@@ -9,8 +9,19 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
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 />
|
||||
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName);
|
||||
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Requests
|
||||
{
|
||||
@@ -14,12 +16,27 @@ namespace CryptoExchange.Net.Requests
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
Timeout = requestTimeout
|
||||
};
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
_httpClient = client;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -30,9 +31,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
private static readonly object _streamIdLock = new();
|
||||
|
||||
private readonly AsyncResetEvent _sendEvent;
|
||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
||||
private readonly ConcurrentQueue<SendItem> _sendBuffer;
|
||||
private readonly SemaphoreSlim _closeSem;
|
||||
private readonly List<DateTime> _outgoingMessages;
|
||||
|
||||
private ClientWebSocket _socket;
|
||||
private CancellationTokenSource _ctsSource;
|
||||
@@ -103,6 +103,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <inheritdoc />
|
||||
public event Action<string>? OnMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<int>? OnRequestSent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<Exception>? OnError;
|
||||
|
||||
@@ -128,10 +131,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
_logger = logger;
|
||||
|
||||
Parameters = websocketParameters;
|
||||
_outgoingMessages = new List<DateTime>();
|
||||
_receivedMessages = new List<ReceiveItem>();
|
||||
_sendEvent = new AsyncResetEvent();
|
||||
_sendBuffer = new ConcurrentQueue<byte[]>();
|
||||
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||
_ctsSource = new CancellationTokenSource();
|
||||
_receivedMessagesLock = new object();
|
||||
|
||||
@@ -270,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Send(string data)
|
||||
public virtual void Send(int id, string data, int weight)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
||||
_sendBuffer.Enqueue(bytes);
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} bytes to send buffer");
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||
_sendEvent.Set();
|
||||
}
|
||||
|
||||
@@ -392,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
try
|
||||
{
|
||||
var limitKey = Uri.ToString() + "/" + Id.ToString();
|
||||
while (true)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
@@ -404,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
while (_sendBuffer.TryDequeue(out var data))
|
||||
{
|
||||
if (Parameters.RatelimitPerSecond != null)
|
||||
if (Parameters.RateLimiters != null)
|
||||
{
|
||||
// Wait for rate limit
|
||||
DateTime? start = null;
|
||||
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
|
||||
foreach(var ratelimiter in Parameters.RateLimiters)
|
||||
{
|
||||
start ??= DateTime.UtcNow;
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).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
|
||||
{
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
_outgoingMessages.Add(DateTime.UtcNow);
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
OnRequestSent?.Invoke(data.Id);
|
||||
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {data.Id} - sent {data.Bytes.Length} bytes");
|
||||
}
|
||||
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>
|
||||
/// Checks if there is no data received for a period longer than the specified timeout
|
||||
/// </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>
|
||||
/// Update the received messages list, removing messages received longer than 3s ago
|
||||
/// </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>
|
||||
/// Received message info
|
||||
/// </summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
internal class PendingRequest
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Func<JToken, bool> Handler { get; }
|
||||
public JToken? Result { get; private set; }
|
||||
public bool Completed { get; private set; }
|
||||
@@ -15,17 +16,22 @@ namespace CryptoExchange.Net.Sockets
|
||||
public TimeSpan Timeout { 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;
|
||||
Event = new AsyncResetEvent(false, false);
|
||||
Timeout = timeout;
|
||||
RequestTimestamp = DateTime.UtcNow;
|
||||
Subscription = subscription;
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource(timeout);
|
||||
public void IsSend()
|
||||
{
|
||||
// Start timeout countdown
|
||||
_cts = new CancellationTokenSource(Timeout);
|
||||
_cts.Token.Register(Fail, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Tag for identificaion
|
||||
/// </summary>
|
||||
public string Tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional properties for this connection
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Properties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If activity is paused
|
||||
@@ -170,12 +175,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
_logger = logger;
|
||||
ApiClient = apiClient;
|
||||
Tag = tag;
|
||||
Properties = new Dictionary<string, object>();
|
||||
|
||||
_pendingRequests = new List<PendingRequest>();
|
||||
_subscriptions = new List<SocketSubscription>();
|
||||
|
||||
_socket = socket;
|
||||
_socket.OnMessage += HandleMessage;
|
||||
_socket.OnRequestSent += HandleRequestSent;
|
||||
_socket.OnOpen += HandleOpen;
|
||||
_socket.OnClose += HandleClose;
|
||||
_socket.OnReconnecting += HandleReconnecting;
|
||||
@@ -278,6 +285,25 @@ namespace CryptoExchange.Net.Sockets
|
||||
_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)
|
||||
{
|
||||
PendingRequest pendingRequest;
|
||||
lock (_pendingRequests)
|
||||
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>
|
||||
/// Process a message received by the socket
|
||||
/// </summary>
|
||||
@@ -312,7 +338,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Check if this message is an answer on any pending requests
|
||||
foreach (var pendingRequest in requests)
|
||||
{
|
||||
|
||||
if (pendingRequest.CheckData(tokenData))
|
||||
{
|
||||
lock (_pendingRequests)
|
||||
@@ -323,12 +348,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Answer to a timed out request, unsub if it is a subscription request
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {pendingRequest.Id} - received data matched to pending request");
|
||||
pendingRequest.Succeed(tokenData);
|
||||
}
|
||||
|
||||
@@ -357,7 +383,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms ({(int)userProcessTime.TotalMilliseconds}ms user code)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -564,45 +590,69 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="timeout">The timeout for response</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="weight">The weight of the message</param>
|
||||
/// <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)
|
||||
{
|
||||
_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>
|
||||
/// Send data over the websocket connection
|
||||
/// </summary>
|
||||
/// <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="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)
|
||||
return Send(str);
|
||||
return Send(requestId, str, weight);
|
||||
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>
|
||||
/// Send string data over the websocket connection
|
||||
/// </summary>
|
||||
/// <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
|
||||
{
|
||||
_socket.Send(data);
|
||||
_socket.Send(requestId, data, weight);
|
||||
return true;
|
||||
}
|
||||
catch(Exception)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
@@ -52,9 +53,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
public TimeSpan? KeepAliveInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of messages to send per second
|
||||
/// The rate limiters for the socket connection
|
||||
/// </summary>
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Origin header value to send in the connection handshake
|
||||
|
||||
@@ -5,20 +5,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="8.0.6" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="5.0.3" />
|
||||
<PackageReference Include="Bittrex.Net" Version="7.0.4" />
|
||||
<PackageReference Include="Bybit.Net" Version="0.0.4" />
|
||||
<PackageReference Include="CoinEx.Net" Version="5.0.3" />
|
||||
<PackageReference Include="FTX.Net" Version="1.0.4" />
|
||||
<PackageReference Include="Huobi.Net" Version="4.0.4" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" />
|
||||
<PackageReference Include="Kucoin.Net" Version="4.0.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00250" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\" />
|
||||
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
@page "/"
|
||||
@inject IBinanceClient binanceClient
|
||||
@inject IBitfinexClient bitfinexClient
|
||||
@inject IBittrexClient bittrexClient
|
||||
@inject IBybitClient bybitClient
|
||||
@inject ICoinExClient coinexClient
|
||||
@inject IFTXClient ftxClient
|
||||
@inject IHuobiClient huobiClient
|
||||
@inject IKrakenClient krakenClient
|
||||
@inject IKucoinClient kucoinClient
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBittrexRestClient bittrexClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject IHuobiRestClient huobiClient
|
||||
@inject IKrakenRestClient krakenClient
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
|
||||
<h3>BTC-USD prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -23,14 +22,13 @@
|
||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
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 ftxTask = ftxClient.TradeApi.ExchangeData.GetSymbolAsync("BTC/USD");
|
||||
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
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)
|
||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||
@@ -42,14 +40,11 @@
|
||||
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
||||
|
||||
if (bybitTask.Result.Success)
|
||||
_prices.Add("Bybit", bybitTask.Result.Data.LastPrice);
|
||||
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
||||
|
||||
if (coinexTask.Result.Success)
|
||||
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
|
||||
|
||||
if (ftxTask.Result.Success)
|
||||
_prices.Add("FTX", ftxTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (huobiTask.Result.Success)
|
||||
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
|
||||
|
||||
|
||||
@@ -4,21 +4,12 @@
|
||||
@inject IBittrexSocketClient bittrexSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@inject IFTXSocketClient ftxSocketClient
|
||||
@inject IHuobiSocketClient huobiSocketClient
|
||||
@inject IKrakenSocketClient krakenSocketClient
|
||||
@inject IKucoinSocketClient kucoinSocketClient
|
||||
@using Binance.Net.Clients.SpotApi
|
||||
@using Bitfinex.Net.Clients.SpotApi
|
||||
@using Bittrex.Net.Clients.SpotApi
|
||||
@using Bybit.Net.Clients.SpotApi
|
||||
@using CoinEx.Net.Clients.SpotApi
|
||||
@using System.Collections.Concurrent
|
||||
@using CryptoExchange.Net.Objects
|
||||
@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
|
||||
|
||||
<h3>ETH-BTC prices, live updates:</h3>
|
||||
@@ -35,15 +26,14 @@
|
||||
{
|
||||
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
||||
{
|
||||
binanceSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bittrexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
||||
bybitSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||
ftxSocketClient.Streams.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("FTX", data.Data.LastPrice ?? 0)),
|
||||
huobiSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||
krakenSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||
kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
@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.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
|
||||
|
||||
<h3>ETH-BTC books, live updates:</h3>
|
||||
@@ -40,22 +45,21 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
|
||||
KucoinClient.SetDefaultOptions(new Kucoin.Net.Objects.KucoinClientOptions
|
||||
{
|
||||
ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE")
|
||||
});
|
||||
KucoinRestClient.SetDefaultOptions(options =>
|
||||
{
|
||||
options.ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE");
|
||||
});
|
||||
|
||||
_books = new Dictionary<string, ISymbolOrderBook>
|
||||
{
|
||||
{ "Binance", new BinanceSpotSymbolOrderBook("ETHBTC") },
|
||||
{ "Bitfinex", new BitfinexSymbolOrderBook("tETHBTC") },
|
||||
{ "Bittrex", new BittrexSymbolOrderBook("ETH-BTC") },
|
||||
{ "Bybit", new BybitSpotSymbolOrderBook("ETHBTC") },
|
||||
{ "CoinEx", new CoinExSpotSymbolOrderBook("ETHBTC") },
|
||||
{ "FTX", new FTXSymbolOrderBook("ETH/BTC") },
|
||||
{ "Huobi", new HuobiSpotSymbolOrderBook("ethbtc") },
|
||||
{ "Kraken", new KrakenSpotSymbolOrderBook("ETH/XBT") },
|
||||
{ "Kucoin", new KucoinSpotSymbolOrderBook("ETH-BTC") },
|
||||
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
@@ -70,7 +74,7 @@
|
||||
{
|
||||
_timer.Stop();
|
||||
_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
|
||||
_ = book.Value.StopAsync();
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
@page "/SpotClient"
|
||||
@inject IBinanceClient binanceClient
|
||||
@inject IBitfinexClient bitfinexClient
|
||||
@inject IBittrexClient bittrexClient
|
||||
@inject IBybitClient bybitClient
|
||||
@inject ICoinExClient coinexClient
|
||||
@inject IFTXClient ftxClient
|
||||
@inject IHuobiClient huobiClient
|
||||
@inject IKrakenClient krakenClient
|
||||
@inject IKucoinClient kucoinClient
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBittrexRestClient bittrexClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject IHuobiRestClient huobiClient
|
||||
@inject IKrakenRestClient krakenClient
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
@using Binance.Net.Clients.SpotApi
|
||||
@using Bitfinex.Net.Clients.SpotApi
|
||||
@using Bittrex.Net.Clients.SpotApi
|
||||
@@ -15,7 +14,6 @@
|
||||
@using CoinEx.Net.Clients.SpotApi
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using CryptoExchange.Net.Interfaces.CommonClients
|
||||
@using FTX.Net.Clients.TradeApi
|
||||
@using Huobi.Net.Clients.SpotApi
|
||||
@using Kraken.Net.Clients.SpotApi
|
||||
@using Kucoin.Net.Clients.SpotApi
|
||||
@@ -37,9 +35,8 @@
|
||||
binanceClient.SpotApi.CommonSpotClient,
|
||||
bitfinexClient.SpotApi.CommonSpotClient,
|
||||
bittrexClient.SpotApi.CommonSpotClient,
|
||||
bybitClient.SpotApi.CommonSpotClient,
|
||||
coinexClient.SpotApi.CommonSpotClient,
|
||||
ftxClient.TradeApi.CommonSpotClient,
|
||||
bybitClient.SpotApiV1.CommonSpotClient,
|
||||
coinexClient.SpotApi.CommonSpotClient,
|
||||
huobiClient.SpotApi.CommonSpotClient,
|
||||
krakenClient.SpotApi.CommonSpotClient,
|
||||
kucoinClient.SpotApi.CommonSpotClient
|
||||
|
||||
@@ -2,15 +2,11 @@ using System.Collections.Generic;
|
||||
using Binance.Net;
|
||||
using Binance.Net.Clients;
|
||||
using Binance.Net.Interfaces.Clients;
|
||||
using Binance.Net.Objects;
|
||||
using Bitfinex.Net;
|
||||
using Bittrex.Net;
|
||||
using Bybit.Net;
|
||||
using CoinEx.Net;
|
||||
using CoinEx.Net.Clients;
|
||||
using CoinEx.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using FTX.Net;
|
||||
using Huobi.Net;
|
||||
using Kraken.Net;
|
||||
using Kucoin.Net;
|
||||
@@ -43,35 +39,18 @@ namespace BlazorClient
|
||||
services.AddServerSideBlazor();
|
||||
|
||||
// Register the clients, options can be provided in the callback parameter
|
||||
services.AddBinance((restClientOptions, socketClientOptions) => {
|
||||
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
|
||||
services.AddBinance(restOptions =>
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
|
||||
LogLevel = LogLevel.Trace
|
||||
});
|
||||
|
||||
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions
|
||||
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
}, socketOptions =>
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
|
||||
socketOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
});
|
||||
|
||||
services.AddTransient<IBinanceClient, BinanceClient>();
|
||||
services.AddScoped<IBinanceSocketClient, BinanceSocketClient>();
|
||||
|
||||
services.AddBitfinex();
|
||||
services.AddBittrex();
|
||||
services.AddBybit();
|
||||
services.AddCoinEx();
|
||||
services.AddFTX();
|
||||
services.AddHuobi();
|
||||
services.AddKraken();
|
||||
services.AddKucoin();
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
@using Bittrex.Net.Interfaces.Clients;
|
||||
@using Bybit.Net.Interfaces.Clients;
|
||||
@using CoinEx.Net.Interfaces.Clients;
|
||||
@using FTX.Net.Interfaces.Clients;
|
||||
@using Huobi.Net.Interfaces.Clients;
|
||||
@using Kraken.Net.Interfaces.Clients;
|
||||
@using Kucoin.Net.Interfaces.Clients;
|
||||
@@ -6,15 +6,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="8.0.6" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="5.0.3" />
|
||||
<PackageReference Include="Bittrex.Net" Version="7.0.4" />
|
||||
<PackageReference Include="Bybit.Net" Version="0.0.4" />
|
||||
<PackageReference Include="CoinEx.Net" Version="5.0.3" />
|
||||
<PackageReference Include="FTX.Net" Version="1.0.4" />
|
||||
<PackageReference Include="Huobi.Net" Version="4.0.4" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" />
|
||||
<PackageReference Include="Kucoin.Net" Version="4.0.3" />
|
||||
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -17,21 +17,21 @@ namespace ConsoleClient.Exchanges
|
||||
|
||||
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));
|
||||
return result.AsDataless();
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, decimal>> GetBalances()
|
||||
{
|
||||
using var client = new BinanceClient();
|
||||
using var client = new BinanceRestClient();
|
||||
var result = await client.SpotApi.Account.GetAccountInfoAsync();
|
||||
return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
||||
{
|
||||
using var client = new BinanceClient();
|
||||
using var client = new BinanceRestClient();
|
||||
var result = await client.SpotApi.Trading.GetOpenOrdersAsync();
|
||||
// Should check result success status here
|
||||
return result.Data.Select(o => new OpenOrder
|
||||
@@ -49,7 +49,7 @@ namespace ConsoleClient.Exchanges
|
||||
|
||||
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);
|
||||
// Should check result success status here
|
||||
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)
|
||||
{
|
||||
using var client = new BinanceClient();
|
||||
using var client = new BinanceRestClient();
|
||||
var result = await client.SpotApi.Trading.PlaceOrderAsync(
|
||||
symbol,
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,10 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Binance.Net.Clients;
|
||||
using Binance.Net.Objects;
|
||||
using Bybit.Net.Clients;
|
||||
using ConsoleClient.Exchanges;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using FTX.Net.Clients;
|
||||
using FTX.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ConsoleClient
|
||||
{
|
||||
@@ -19,20 +17,18 @@ namespace ConsoleClient
|
||||
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
|
||||
{
|
||||
{ "Binance", new BinanceExchange() },
|
||||
{ "FTX", new FTXExchange() }
|
||||
{ "Bybit", new BybitExchange() }
|
||||
};
|
||||
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
||||
BinanceRestClient.SetDefaultOptions(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
|
||||
options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
|
||||
});
|
||||
FTXClient.SetDefaultOptions(new FTXClientOptions
|
||||
BybitRestClient.SetDefaultOptions(options =>
|
||||
{
|
||||
LogLevel = LogLevel.Trace,
|
||||
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
|
||||
options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
|
||||
});
|
||||
|
||||
while (true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# CryptoExchange.Net
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml)  
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) [](https://www.nuget.org/packages/CryptoExchange.Net)
|
||||
|
||||
CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections.
|
||||
|
||||
@@ -18,21 +18,43 @@ Use one of the following following referral links to signup to a new exchange to
|
||||
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
|
||||
[Bybit](https://partner.bybit.com/b/jkorf)
|
||||
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
|
||||
[FTX](https://ftx.com/referrals#a=31620192)
|
||||
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
|
||||
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
|
||||
|
||||
### 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.
|
||||
|
||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
||||
**Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
|
||||
**Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
|
||||
|
||||
### Sponsor
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 6.1.3 - 18 Sep 2023
|
||||
* Fix for concurrency exception in socket subscription
|
||||
|
||||
* Version 6.1.2 - 11 Sep 2023
|
||||
* Added support for multiple of the same ratelimiting type in the same rate limiter
|
||||
* Fixed nullreference on rate limit error if no Retry-After header is returned
|
||||
|
||||
* Version 6.1.1 - 04 Sep 2023
|
||||
* Fixes for json converters
|
||||
|
||||
* 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
|
||||
* 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
|
||||
|
||||
+19
-19
@@ -11,15 +11,15 @@ Each implementation generally provides two different clients, which will be the
|
||||
The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this:
|
||||
|
||||
- [ExchangeName]RestClient
|
||||
- SpotApi
|
||||
- Account
|
||||
- ExchangeData
|
||||
- Trading
|
||||
- FuturesApi
|
||||
- Account
|
||||
- ExchangeData
|
||||
- Trading
|
||||
|
||||
- SpotApi
|
||||
- Account
|
||||
- ExchangeData
|
||||
- Trading
|
||||
- FuturesApi
|
||||
- Account
|
||||
- ExchangeData
|
||||
- Trading
|
||||
|
||||
This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints.
|
||||
|
||||
*Requesting ticker info on the spot API*
|
||||
@@ -64,8 +64,8 @@ When processing the result of a call it should always be checked for success. No
|
||||
var callResult = await kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
|
||||
if(!callResult.Success)
|
||||
{
|
||||
Console.WriteLine("Request failed: " + callResult.Error);
|
||||
return;
|
||||
Console.WriteLine("Request failed: " + callResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Result: " + callResult.Data);
|
||||
@@ -78,8 +78,8 @@ Just like the Rest client is divided in Rest Api clients, the Socket client is d
|
||||
```csharp
|
||||
|
||||
- KucoinSocketClient
|
||||
- SpotStreams
|
||||
- FuturesStreams
|
||||
- SpotStreams
|
||||
- FuturesStreams
|
||||
|
||||
```
|
||||
*Subscribing to updates for all tickers on the Spot Api*
|
||||
@@ -95,7 +95,7 @@ await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandle
|
||||
|
||||
private static void DataHandler(DataEvent<KucoinStreamTick> updateData)
|
||||
{
|
||||
// Process updateData
|
||||
// Process updateData
|
||||
}
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ private static void DataHandler(DataEvent<KucoinStreamTick> updateData)
|
||||
```csharp
|
||||
await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData =>
|
||||
{
|
||||
// Process updateData
|
||||
// Process updateData
|
||||
});
|
||||
```
|
||||
|
||||
@@ -122,16 +122,16 @@ Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. T
|
||||
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||
if(!subscriptionResult.Success)
|
||||
{
|
||||
Console.WriteLine("Failed to connect: " + subscriptionResult.Error);
|
||||
return;
|
||||
Console.WriteLine("Failed to connect: " + subscriptionResult.Error);
|
||||
return;
|
||||
}
|
||||
subscriptionResult.Data.ConnectionLost += () =>
|
||||
{
|
||||
Console.WriteLine("Connection lost");
|
||||
Console.WriteLine("Connection lost");
|
||||
};
|
||||
subscriptionResult.Data.ConnectionRestored += (time) =>
|
||||
{
|
||||
Console.WriteLine("Connection restored");
|
||||
Console.WriteLine("Connection restored");
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ private void SomeMethod()
|
||||
{
|
||||
var socketClient = new BinanceSocketClient();
|
||||
socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
|
||||
// Handle data
|
||||
// Handle data
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -41,7 +41,7 @@ private BinanceSocketClient _socketClient = new BinanceSocketClient();
|
||||
private void SomeMethod()
|
||||
{
|
||||
_socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
|
||||
// Handle data
|
||||
// Handle data
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ Yes, generally these are all supported and can be configured by setting the Envi
|
||||
```csharp
|
||||
var client = new BinanceRestClient(options =>
|
||||
{
|
||||
options.Environment = BinanceEnvironment.Testnet;
|
||||
options.Environment = BinanceEnvironment.Testnet;
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
+5
-5
@@ -10,7 +10,7 @@ The library offers extensive logging, which depends on the dotnet `Microsoft.Ext
|
||||
```csharp
|
||||
IServiceCollection services = new ServiceCollection();
|
||||
services
|
||||
.AddBinance()
|
||||
.AddBinance()
|
||||
.AddLogging(options =>
|
||||
{
|
||||
options.SetMinimumLevel(LogLevel.Trace);
|
||||
@@ -66,8 +66,8 @@ IServiceCollection serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddBinance();
|
||||
serviceCollection.AddLogging(options =>
|
||||
{
|
||||
options.SetMinimumLevel(LogLevel.Trace);
|
||||
options.AddConsole();
|
||||
options.SetMinimumLevel(LogLevel.Trace);
|
||||
options.AddConsole();
|
||||
}).BuildServiceProvider();
|
||||
|
||||
var client = serviceCollection.GetRequiredService<IBinanceRestClient>();
|
||||
@@ -91,7 +91,7 @@ By default the `OriginalData` property in the `WebCallResult`/`DataEvent` object
|
||||
```csharp
|
||||
var client = new BinanceClient(options =>
|
||||
{
|
||||
options.OutputOriginalData = true
|
||||
options.OutputOriginalData = true
|
||||
});
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ var originallyReceivedData = tickerResult.OriginalData;
|
||||
|
||||
// Socket update
|
||||
await client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
|
||||
var originallyRecievedData = update.OriginalData;
|
||||
var originallyRecievedData = update.OriginalData;
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ The HttpClient will now be received by the DI container instead of having to pas
|
||||
```csharp
|
||||
var client = new BinanceClient(new BinanceClientOptions(){
|
||||
OutputOriginalData = true,
|
||||
SpotApiOptions = new RestApiOptions {
|
||||
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
|
||||
}
|
||||
SpotApiOptions = new RestApiOptions {
|
||||
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
|
||||
}
|
||||
// Other options
|
||||
});
|
||||
```
|
||||
@@ -38,7 +38,7 @@ var client = new BinanceClient(new BinanceClientOptions(){
|
||||
```csharp
|
||||
var client = new BinanceClient(options => {
|
||||
options.OutputOriginalData = true;
|
||||
options.Environment = BinanceEnvironment.Testnet;
|
||||
options.Environment = BinanceEnvironment.Testnet;
|
||||
// Other options
|
||||
});
|
||||
```
|
||||
@@ -53,10 +53,10 @@ With the change in options providing the DI extension methods for the IServiceCo
|
||||
```csharp
|
||||
builder.Services.AddKucoin((restOpts, socketOpts) =>
|
||||
{
|
||||
restOpts.LogLevel = LogLevel.Debug;
|
||||
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
socketOpts.LogLevel = LogLevel.Debug;
|
||||
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
restOpts.LogLevel = LogLevel.Debug;
|
||||
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
socketOpts.LogLevel = LogLevel.Debug;
|
||||
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||
}, ServiceLifetime.Singleton);
|
||||
```
|
||||
|
||||
|
||||
+12
-8
@@ -12,8 +12,10 @@ Each implementation can be configured using client options. There are 2 ways to
|
||||
|
||||
BinanceClient.SetDefaultOptions(options =>
|
||||
{
|
||||
options.OutputOriginalData = true;
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
options.OutputOriginalData = true;
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
// Override the api credentials for the Spot API
|
||||
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||
});
|
||||
|
||||
```
|
||||
@@ -23,8 +25,10 @@ BinanceClient.SetDefaultOptions(options =>
|
||||
|
||||
var client = new BinanceClient(options =>
|
||||
{
|
||||
options.OutputOriginalData = true;
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
options.OutputOriginalData = true;
|
||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
// Override the api credentials for the Spot API
|
||||
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||
});
|
||||
|
||||
```
|
||||
@@ -34,12 +38,12 @@ When calling `SetDefaultOptions` each client created after that will use the opt
|
||||
|
||||
BinanceClient.SetDefaultOptions(options =>
|
||||
{
|
||||
options.OutputOriginalData = true;
|
||||
options.OutputOriginalData = true;
|
||||
});
|
||||
|
||||
var client = new BinanceClient(options =>
|
||||
{
|
||||
options.OutputOriginalData = false;
|
||||
options.OutputOriginalData = false;
|
||||
});
|
||||
|
||||
```
|
||||
@@ -54,8 +58,8 @@ The options are divided in two categories. The basic options, which will apply t
|
||||
|
||||
var client = new BinanceRestClient(options =>
|
||||
{
|
||||
options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
|
||||
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||
options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
|
||||
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
+5
-5
@@ -18,15 +18,15 @@ book.OnStatusChange += (oldState, newState) => Console.WriteLine($"State changed
|
||||
var startResult = await book.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
Console.WriteLine("Failed to start order book: " + startResult.Error);
|
||||
return;
|
||||
Console.WriteLine("Failed to start order book: " + startResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
while(true)
|
||||
{
|
||||
Console.Clear();
|
||||
Console.WriteLine(book.ToString(3);
|
||||
await Task.Delay(500);
|
||||
Console.Clear();
|
||||
Console.WriteLine(book.ToString(3);
|
||||
await Task.Delay(500);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
+8
-8
@@ -13,12 +13,12 @@ A rate limiter can be configured in the options like so:
|
||||
```csharp
|
||||
new ClientOptions
|
||||
{
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||
RateLimiters = new List<IRateLimiter>
|
||||
{
|
||||
new RateLimiter()
|
||||
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
||||
}
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||
RateLimiters = new List<IRateLimiter>
|
||||
{
|
||||
new RateLimiter()
|
||||
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -26,8 +26,8 @@ This will add a rate limiter for 50 requests per 10 seconds.
|
||||
A rate limiter can have multiple limits:
|
||||
```csharp
|
||||
new RateLimiter()
|
||||
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
||||
.AddEndpointLimit("/api/order", 10, TimeSpan.FromSeconds(2))
|
||||
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
||||
.AddEndpointLimit("/api/order", 10, TimeSpan.FromSeconds(2))
|
||||
```
|
||||
This adds another limit of 10 requests per 2 seconds in addition to the 50 requests per 10 seconds limit.
|
||||
These are the available rate limit configurations:
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ title: Home
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml)  
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) [](https://www.nuget.org/packages/CryptoExchange.Net)
|
||||
|
||||
The CryptoExchange.Net library is a base package for exchange API implementations. It offers base classes for creating clients for exchange API's. Basing exchange implementation on the common CryptoExchange.Net library allows for ease of implementation for new exchanges, as only the endpoints and models have to implemented, but not all systems around requests and connections, and it makes it easier for users to implement a new library in their code base as all base principles and configuration are the same for different exchanges.
|
||||
|
||||
@@ -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/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/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**
|
||||
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/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/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/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|
|
||||
|
||||
Reference in New Issue
Block a user