1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 16:32:57 +00:00

Compare commits

...

22 Commits

Author SHA1 Message Date
Jkorf d433ff7475 Updated to version 9.9.0 2025-10-06 13:47:21 +02:00
Jkorf 71957037d0 Updated CryptoExchange.Net version to 9.8.0 2025-10-06 13:45:47 +02:00
Jkorf bcdcdbbd4e Updated to version 9.9.0 2025-10-06 13:26:23 +02:00
Jkorf 1ece13f5bc Updated socket Subscription status handling, fixing timing issue for connection events and adding SubscriptionStatusChanged event 2025-10-06 13:22:40 +02:00
Jkorf da70ba6ec7 Added Aster reference 2025-10-06 10:37:28 +02:00
Jkorf a832f0e4d4 Updated to version 9.8.0 2025-09-30 12:06:14 +02:00
Jkorf 6ab4d005a0 Updated CryptoExchange.Net version to 9.8.0 2025-09-30 12:04:28 +02:00
Jkorf 18dc935038 Updated to version 9.8.0 2025-09-30 11:56:00 +02:00
Jkorf bb3a534f75 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-09-30 11:50:16 +02:00
nils2525 649ba370c6 Fixed EnumConverter to allow mapping empty string values (#253) 2025-09-30 11:49:58 +02:00
Jkorf 51732c5ce6 Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect 2025-09-29 14:43:39 +02:00
Jkorf 0ba7b46680 Added ContractAddress to SharedAsset model 2025-09-29 13:51:59 +02:00
Jkorf 94dfbb7b9e Fixed ExchangeHelpers.AdjustValueStep high precision calculation 2025-09-29 13:51:46 +02:00
Jkorf b8b7512b35 Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed 2025-09-29 10:07:28 +02:00
Jkorf aba6b773ce Added ITrackerFactory interface 2025-09-17 10:55:48 +02:00
Jkorf d88fb0d356 Added BloFin to ReadMe and examples 2025-09-17 10:01:17 +02:00
Jkorf b8c6d55156 CryptoManager.Net reference 2025-09-02 11:43:44 +02:00
Jkorf d9a5481db2 Updated to version 9.7.0 2025-09-01 13:37:16 +02:00
Jkorf 6a8bb42c0e Updated CryptoExchange.Net for CryptoExchange.Net.Protobuf version to 9.7.0 2025-09-01 13:35:24 +02:00
Jkorf 2445f001ab Updated to version 9.7.0 2025-09-01 13:18:16 +02:00
Jkorf c84fa9ac32 Fixed test 2025-09-01 13:16:20 +02:00
Jkorf d44a11c44e HttpVersion update
Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
Added REST client option for selecting HTTP protocol version
Added REST client option for HTTP client keep alive interval
Added HttpVersion to WebCallResult responses
Updated request logic to default to using HTTP version 2.0 for dotnet core
2025-09-01 10:12:59 +02:00
40 changed files with 695 additions and 217 deletions
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.6.0</FileVersion>
<PackageVersion>9.9.0</PackageVersion>
<AssemblyVersion>9.9.0</AssemblyVersion>
<FileVersion>9.9.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
<PackageReference Include="CryptoExchange.Net" Version="9.9.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
+9
View File
@@ -5,6 +5,15 @@
Protobuf support for CryptoExchange.Net.
## Release notes
* Version 9.9.0 - 06 Oct 2025
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
@@ -5,6 +5,7 @@ using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@@ -113,6 +114,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
@@ -143,6 +145,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
await sub;
// assert
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
}
[TestCase()]
@@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
await sub;
// assert
Assert.That(client.SubClient.TestSubscription.Confirmed);
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
}
}
}
@@ -60,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
@@ -69,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
@@ -90,12 +90,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
}
@@ -118,13 +118,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
}
}
+36 -17
View File
@@ -106,7 +106,7 @@ namespace CryptoExchange.Net.Clients
options,
apiOptions)
{
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
RequestFactory.Configure(options, httpClient);
}
/// <summary>
@@ -388,7 +388,7 @@ namespace CryptoExchange.Net.Clients
queryString = $"?{queryString}";
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(definition.Method, uri, requestId);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
foreach (var header in requestConfiguration.Headers)
@@ -443,9 +443,6 @@ namespace CryptoExchange.Net.Clients
{
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseLength = response.ContentLength;
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
@@ -475,18 +472,18 @@ namespace CryptoExchange.Net.Clients
if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
}
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
if (!valid)
{
// Invalid json
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
}
// Json response received
@@ -503,33 +500,55 @@ namespace CryptoExchange.Net.Clients
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
}
else
{
// Request timed out
var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
}
catch (ArgumentException argumentException)
{
if (argumentException.Message.StartsWith("Only HTTP/"))
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
catch (NotSupportedException notSupportedException)
{
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
finally
{
accessor?.Clear();
@@ -674,21 +693,21 @@ namespace CryptoExchange.Net.Clients
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{
timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
var localTime = DateTime.UtcNow;
@@ -717,7 +736,7 @@ namespace CryptoExchange.Net.Clients
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
private bool ShouldCache(RequestDefinition definition)
+52 -16
View File
@@ -227,7 +227,7 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
@@ -269,6 +269,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
subscription.Status = SubscriptionStatus.Subscribing;
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null)
@@ -279,7 +280,7 @@ namespace CryptoExchange.Net.Clients
{
waitEvent?.Set();
var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Confirmed)
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
@@ -287,6 +288,7 @@ namespace CryptoExchange.Net.Clients
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
subscription.Status = SubscriptionStatus.Pending;
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
@@ -295,7 +297,7 @@ namespace CryptoExchange.Net.Clients
subscription.HandleSubQueryResponse(subQuery.Response!);
}
subscription.Confirmed = true;
subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
@@ -343,7 +345,7 @@ namespace CryptoExchange.Net.Clients
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<THandlerResponse>(default);
@@ -494,25 +496,56 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="ct">Cancellation token</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
{
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
&& s.Value.Connected);
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value)
.ToList();
SocketConnection connection;
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError());
}
break;
}
delayed = true;
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError());
}
if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList();
SocketConnection? connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
else
{
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
@@ -520,9 +553,12 @@ namespace CryptoExchange.Net.Clients
if (connection != null)
{
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(connection);
}
}
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
@@ -716,7 +752,7 @@ namespace CryptoExchange.Net.Clients
{
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult)
return socketResult.AsDataless();
@@ -813,7 +849,7 @@ namespace CryptoExchange.Net.Clients
cs.SubscriptionStates.ForEach(subState =>
{
sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
});
@@ -140,10 +140,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
};
if (string.IsNullOrEmpty(stringValue))
if (stringValue is null)
return null;
if (!GetValue(enumType, stringValue!, out var result))
if (!GetValue(enumType, stringValue, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{
@@ -204,6 +204,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false;
}
if (String.IsNullOrEmpty(value))
{
// An empty/null value will always fail when parsing, so just return here
result = default;
return false;
}
try
{
// If no explicit mapping is found try to parse string
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.6.0</FileVersion>
<PackageVersion>9.9.0</PackageVersion>
<AssemblyVersion>9.9.0</AssemblyVersion>
<FileVersion>9.9.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
-2
View File
@@ -88,8 +88,6 @@ namespace CryptoExchange.Net
value -= offset;
else value += (step.Value - offset);
}
value = RoundDown(value, 8);
return value.Normalize();
}
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
Uri Uri { get; }
/// <summary>
/// HTTP protocol version
/// </summary>
Version HttpVersion { get; }
/// <summary>
/// internal request id for tracing
/// </summary>
int RequestId { get; }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System;
using System.Net.Http;
@@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Create a request for an uri
/// </summary>
/// <param name="method"></param>
/// <param name="uri"></param>
/// <param name="requestId"></param>
/// <returns></returns>
IRequest Create(HttpMethod method, Uri uri, int requestId);
IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId);
/// <summary>
/// Configure the requests created by this factory
/// </summary>
/// <param name="requestTimeout">Request timeout to use</param>
/// <param name="options">Rest client options</param>
/// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
void Configure(RestExchangeOptions options, HttpClient? httpClient = null);
/// <summary>
/// Update settings
/// </summary>
/// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
/// <param name="httpKeepAliveInterval">Http client keep alive interval</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval);
}
}
+7 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
HttpStatusCode StatusCode { get; }
/// <summary>
/// Http protocol version
/// </summary>
Version HttpVersion { get; }
/// <summary>
/// Whether the status code indicates a success status
/// </summary>
@@ -0,0 +1,45 @@
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.Klines;
using CryptoExchange.Net.Trackers.Trades;
using System;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Tracker factory
/// </summary>
public interface ITrackerFactory
{
/// <summary>
/// Whether the factory supports creating a KlineTracker instance for this symbol and interval
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="interval">The kline interval</param>
bool CanCreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval);
/// <summary>
/// Create a new kline tracker
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="interval">Kline interval</param>
/// <param name="limit">The max amount of klines to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <returns></returns>
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
/// <summary>
/// Whether the factory supports creating a TradeTracker instance for this symbol
/// </summary>
/// <param name="symbol">The symbol</param>
bool CanCreateTradeTracker(SharedSymbol symbol);
/// <summary>
/// Create a new trade tracker for a symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="limit">The max amount of trades to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <returns></returns>
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
}
}
+57 -1
View File
@@ -1,5 +1,8 @@
using System;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net
@@ -43,5 +46,58 @@ namespace CryptoExchange.Net
return clientOrderId;
}
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
{
#if NET5_0_OR_GREATER
var socketHandler = new SocketsHttpHandler();
try
{
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
{
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
}
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
socketHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
return socketHandler;
#else
var httpHandler = new HttpClientHandler();
try
{
httpHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
httpHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
httpHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
return httpHandler;
#endif
}
}
}
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
private static readonly Action<ILogger, Exception?> _timeoutWaitingForReconnectingSocket;
private static readonly Action<ILogger, long, Exception?> _waitedForReconnectingSocket;
static SocketApiClientLoggingExtension()
{
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
"Timeout while waiting for existing socket reconnection, failing request");
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
LogLevel.Trace,
new EventId(3020, "WaitedForReconnectingSocket"),
"Waited for reconnecting socket for {Timespan}ms");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_addingRetryAfterGuard(logger, retryAfter, null);
}
public static void TimeoutWaitingForReconnectingSocket(this ILogger logger)
{
_timeoutWaitingForReconnectingSocket(logger, null);
}
public static void WaitedForReconnectingSocket(this ILogger logger, long milliseconds)
{
_waitedForReconnectingSocket(logger, milliseconds, null);
}
}
}
+24 -23
View File
@@ -205,6 +205,11 @@ namespace CryptoExchange.Net.Objects
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
@@ -251,6 +256,7 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime,
string? originalData,
@@ -262,6 +268,7 @@ namespace CryptoExchange.Net.Objects
Error? error) : base(error)
{
ResponseStatusCode = code;
HttpVersion = httpVersion;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestId = requestId;
@@ -286,7 +293,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult AsError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
@@ -297,7 +304,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
}
/// <summary>
@@ -334,7 +341,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
}
/// <inheritdoc />
@@ -355,6 +362,11 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
@@ -403,21 +415,9 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// Create a new result
/// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="responseLength"></param>
/// <param name="originalData"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="dataSource"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
@@ -431,6 +431,7 @@ namespace CryptoExchange.Net.Objects
[AllowNull] T data,
Error? error) : base(data, originalData, error)
{
HttpVersion = httpVersion;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
@@ -450,7 +451,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult AsDataless()
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
@@ -458,14 +459,14 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
@@ -475,7 +476,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
}
/// <summary>
@@ -486,7 +487,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
}
/// <summary>
@@ -498,7 +499,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
}
/// <summary>
@@ -569,7 +570,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
internal WebCallResult<T> Cached()
{
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
}
/// <inheritdoc />
+27
View File
@@ -267,4 +267,31 @@ namespace CryptoExchange.Net.Objects
Succeed
}
/// <summary>
/// Subscription status
/// </summary>
public enum SubscriptionStatus
{
/// <summary>
/// Pending, waiting before (re)subscription can be started
/// </summary>
Pending,
/// <summary>
/// Currently (re)subscribing, will start producing updates soon if subscription is successful
/// </summary>
Subscribing,
/// <summary>
/// Subscribed and listening to updates
/// </summary>
Subscribed,
/// <summary>
/// Subscription is being closed and will stop producing updates
/// </summary>
Closing,
/// <summary>
/// Subscription is closed and will no long produce updates
/// </summary>
Closed
}
}
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Authentication;
using System;
using System.Net;
using System.Net.Http;
namespace CryptoExchange.Net.Objects.Options
{
@@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// The HTTP protocol version to use, typically 2.0 or 1.1
/// </summary>
public Version HttpVersion { get; set; }
#if NET5_0_OR_GREATER
= new Version(2, 0);
#else
= new Version(1, 1);
#endif
/// <summary>
/// Http client keep alive interval for keeping connections open
/// </summary>
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
/// <summary>
/// Set the values of this options on the target options
/// </summary>
@@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options
item.RateLimitingBehaviour = RateLimitingBehaviour;
item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge;
item.HttpVersion = HttpVersion;
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
return item;
}
}
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects.Sockets
@@ -12,13 +14,27 @@ namespace CryptoExchange.Net.Objects.Sockets
private readonly SocketConnection _connection;
private readonly Subscription _listener;
private object _eventLock = new object();
private bool _connectionEventsSubscribed = true;
private List<Action> _connectionClosedEventHandlers = new List<Action>();
private List<Action> _connectionLostEventHandlers = new List<Action>();
private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>();
private List<Action<TimeSpan>> _connectionRestoredEventHandlers = new List<Action<TimeSpan>>();
private List<Action> _activityPausedEventHandlers = new List<Action>();
private List<Action> _activityUnpausedEventHandlers = new List<Action>();
/// <summary>
/// Event when the status of the subscription changes
/// </summary>
public event Action<SubscriptionStatus>? SubscriptionStatusChanged;
/// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible.
/// </summary>
public event Action ConnectionLost
{
add => _connection.ConnectionLost += value;
remove => _connection.ConnectionLost -= value;
add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
}
/// <summary>
@@ -26,8 +42,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action ConnectionClosed
{
add => _connection.ConnectionClosed += value;
remove => _connection.ConnectionClosed -= value;
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
}
/// <summary>
@@ -35,8 +51,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action<Error> ResubscribingFailed
{
add => _connection.ResubscribingFailed += value;
remove => _connection.ResubscribingFailed -= value;
add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
}
/// <summary>
@@ -46,8 +62,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action<TimeSpan> ConnectionRestored
{
add => _connection.ConnectionRestored += value;
remove => _connection.ConnectionRestored -= value;
add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
}
/// <summary>
@@ -55,8 +71,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action ActivityPaused
{
add => _connection.ActivityPaused += value;
remove => _connection.ActivityPaused -= value;
add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
}
/// <summary>
@@ -64,8 +80,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public event Action ActivityUnpaused
{
add => _connection.ActivityUnpaused += value;
remove => _connection.ActivityUnpaused -= value;
add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
}
/// <summary>
@@ -95,7 +111,128 @@ namespace CryptoExchange.Net.Objects.Sockets
public UpdateSubscription(SocketConnection connection, Subscription subscription)
{
_connection = connection;
_connection.ConnectionClosed += HandleConnectionClosedEvent;
_connection.ConnectionLost += HandleConnectionLostEvent;
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
_connection.ActivityPaused += HandlePausedEvent;
_connection.ActivityUnpaused += HandleUnpausedEvent;
_listener = subscription;
_listener.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
private void UnsubscribeConnectionEvents()
{
lock (_eventLock)
{
if (!_connectionEventsSubscribed)
return;
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
_connectionEventsSubscribed = false;
}
}
private void HandleConnectionClosedEvent()
{
UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting
if (!_listener.IsClosingConnection)
return;
List<Action> handlers;
lock (_eventLock)
handlers = _connectionClosedEventHandlers.ToList();
foreach(var callback in handlers)
callback();
}
private void HandleConnectionLostEvent()
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers;
lock (_eventLock)
handlers = _connectionLostEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action<TimeSpan>> handlers;
lock (_eventLock)
handlers = _connectionRestoredEventHandlers.ToList();
foreach (var callback in handlers)
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action<Error>> handlers;
lock (_eventLock)
handlers = _resubscribeFailedEventHandlers.ToList();
foreach (var callback in handlers)
callback(error);
}
private void HandlePausedEvent()
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers;
lock (_eventLock)
handlers = _activityPausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleUnpausedEvent()
{
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers;
lock (_eventLock)
handlers = _activityUnpausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
/// <summary>
+6 -1
View File
@@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public Uri Uri => _request.RequestUri!;
/// <inheritdoc />
public Version HttpVersion => _request.Version!;
/// <inheritdoc />
public int RequestId { get; }
@@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
{
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
return new Response(response);
}
}
}
+16 -26
View File
@@ -3,6 +3,7 @@ using System.Net;
using System.Net.Http;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Requests
{
@@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests
private HttpClient? _httpClient;
/// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
public void Configure(RestExchangeOptions options, HttpClient? client = null)
{
if (client == null)
client = CreateClient(proxy, requestTimeout);
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
_httpClient = client;
}
/// <inheritdoc />
public IRequest Create(HttpMethod method, Uri uri, int requestId)
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
{
if (_httpClient == null)
throw new InvalidOperationException("Cant create request before configuring http client");
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Version = httpRequestVersion;
#if NET5_0_OR_GREATER
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
#endif
return new Request(requestMessage, _httpClient, requestId);
}
/// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{
_httpClient = CreateClient(proxy, requestTimeout);
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
}
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{
var handler = new HttpClientHandler();
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
var client = new HttpClient(handler)
{
Timeout = requestTimeout
Timeout = requestTimeout
};
return client;
}
}
}
+5 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public HttpStatusCode StatusCode => _response.StatusCode;
/// <inheritdoc />
public Version HttpVersion => _response.Version;
/// <inheritdoc />
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis
WebCallResult<T> result,
INextPageToken? nextPageToken = null) :
base(result.ResponseStatusCode,
result.HttpVersion,
result.ResponseHeaders,
result.ResponseTime,
result.ResponseLength,
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis
WebCallResult<T> result,
INextPageToken? nextPageToken = null) :
base(result.ResponseStatusCode,
result.HttpVersion,
result.ResponseHeaders,
result.ResponseTime,
result.ResponseLength,
@@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis
string exchange,
TradingMode[]? dataTradeModes,
HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
@@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis
Error? error,
INextPageToken? nextPageToken = null) : base(
code,
httpVersion,
responseHeaders,
responseTime,
responseLength,
@@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
/// <returns></returns>
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
{
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
}
/// <inheritdoc />
@@ -67,6 +67,10 @@ namespace CryptoExchange.Net.SharedApis
/// Min number of confirmations
/// </summary>
public int? MinConfirmations { get; set; }
/// <summary>
/// The contract address
/// </summary>
public string? ContractAddress { get; set; }
/// <summary>
/// ctor
+19 -14
View File
@@ -296,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
{
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
subscription.Reset();
foreach (var query in _listeners.OfType<Query>().ToList())
@@ -527,10 +527,10 @@ namespace CryptoExchange.Net.Sockets
continue;
}
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
if (processor is Subscription subscriptionProcessor && subscriptionProcessor.Status == SubscriptionStatus.Subscribing)
{
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
subscriptionProcessor.Confirmed = true;
subscriptionProcessor.Status = SubscriptionStatus.Subscribed;
if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed)
// If this subscription has a query waiting for a timeout (success if there is no error response)
// then time it out now as the data is being received, so we assume it's successful
@@ -657,13 +657,16 @@ namespace CryptoExchange.Net.Sockets
public async Task CloseAsync(Subscription subscription)
{
// If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues
while (subscription.IsResubscribing)
while (subscription.Status == SubscriptionStatus.Subscribing)
await Task.Delay(50).ConfigureAwait(false);
subscription.Closed = true;
subscription.Status = SubscriptionStatus.Closing;
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
{
subscription.Status = SubscriptionStatus.Closed;
return;
}
_logger.ClosingSubscription(SocketId, subscription.Id);
if (subscription.CancellationTokenRegistration.HasValue)
@@ -675,7 +678,7 @@ namespace CryptoExchange.Net.Sockets
bool shouldCloseConnection;
lock (_listenersLock)
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
if (!anyDuplicateSubscription)
{
@@ -693,6 +696,7 @@ namespace CryptoExchange.Net.Sockets
if (Status == SocketStatus.Closing)
{
subscription.Status = SubscriptionStatus.Closed;
_logger.AlreadyClosing(SocketId);
return;
}
@@ -700,12 +704,15 @@ namespace CryptoExchange.Net.Sockets
if (shouldCloseConnection)
{
Status = SocketStatus.Closing;
subscription.IsClosingConnection = true;
_logger.ClosingNoMoreSubscriptions(SocketId);
await CloseAsync().ConfigureAwait(false);
}
lock (_listenersLock)
_listeners.Remove(subscription);
subscription.Status = SubscriptionStatus.Closed;
}
/// <summary>
@@ -989,7 +996,7 @@ namespace CryptoExchange.Net.Sockets
List<Subscription> subList;
lock (_listenersLock)
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
if (subList.Count == 0)
break;
@@ -998,34 +1005,32 @@ namespace CryptoExchange.Net.Sockets
foreach (var subscription in subList)
{
subscription.ConnectionInvocations = 0;
if (subscription.Closed)
if (!subscription.Active)
// Can be closed during resubscribing
continue;
subscription.IsResubscribing = true;
subscription.Status = SubscriptionStatus.Subscribing;
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result)
{
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
subscription.IsResubscribing = false;
subscription.Status = SubscriptionStatus.Pending;
return result;
}
var subQuery = subscription.CreateSubscriptionQuery(this);
if (subQuery == null)
{
subscription.IsResubscribing = false;
subscription.Status = SubscriptionStatus.Subscribed;
continue;
}
var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{
subscription.IsResubscribing = false;
subscription.Status = r.Result.Success ? SubscriptionStatus.Subscribed: SubscriptionStatus.Pending;
subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set();
if (r.Result.Success)
subscription.Confirmed = true;
return r.Result;
}));
}
+27 -11
View File
@@ -34,21 +34,33 @@ namespace CryptoExchange.Net.Sockets
/// Is it a user subscription
/// </summary>
public bool UserSubscription { get; set; }
private SubscriptionStatus _status;
/// <summary>
/// Has the subscription been confirmed
/// Current subscription status
/// </summary>
public bool Confirmed { get; set; }
public SubscriptionStatus Status
{
get => _status;
set
{
if (_status == value)
return;
_status = value;
Task.Run(() => StatusChanged?.Invoke(value));
}
}
/// <summary>
/// Is the subscription closed
/// Whether the subscription is active
/// </summary>
public bool Closed { get; set; }
public bool Active => Status != SubscriptionStatus.Closing && Status != SubscriptionStatus.Closed;
/// <summary>
/// Is the subscription currently resubscribing
/// Whether the unsubscribing of this subscription lead to the closing of the connection
/// </summary>
public bool IsResubscribing { get; set; }
public bool IsClosingConnection { get; set; }
/// <summary>
/// Logger
@@ -74,6 +86,10 @@ namespace CryptoExchange.Net.Sockets
/// Exception event
/// </summary>
public event Action<Exception>? Exception;
/// <summary>
/// Listener unsubscribed event
/// </summary>
public event Action<SubscriptionStatus>? StatusChanged;
/// <summary>
/// Subscription topic
@@ -163,7 +179,7 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public void Reset()
{
Confirmed = false;
Status = SubscriptionStatus.Pending;
DoHandleReset();
}
@@ -185,12 +201,12 @@ namespace CryptoExchange.Net.Sockets
/// State of this subscription
/// </summary>
/// <param name="Id">The id of the subscription</param>
/// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param>
/// <param name="Status">Subscription status</param>
/// <param name="Invocations">Number of times this subscription got a message</param>
/// <param name="ListenMatcher">Matcher for this subscription</param>
public record SubscriptionState(
int Id,
bool Confirmed,
SubscriptionStatus Status,
int Invocations,
MessageMatcher ListenMatcher
);
@@ -201,7 +217,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
public SubscriptionState GetState()
{
return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher);
return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher);
}
}
@@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="authenticated"></param>
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
{
Confirmed = true;
Status = SubscriptionStatus.Subscribed;
}
/// <inheritdoc />
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Testing.Implementations
public Uri Uri { get; set; }
public Version HttpVersion { get; set; }
public int RequestId { get; set; }
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System;
using System.Net.Http;
@@ -14,11 +15,11 @@ namespace CryptoExchange.Net.Testing.Implementations
_request = request;
}
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null)
{
public void Configure(RestExchangeOptions options, HttpClient? client)
{
}
public IRequest Create(HttpMethod method, Uri uri, int requestId)
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
{
_request.Method = method;
_request.Uri = uri;
@@ -26,6 +27,6 @@ namespace CryptoExchange.Net.Testing.Implementations
return _request;
}
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval) {}
}
}
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.Testing.Implementations
private readonly Stream _response;
public HttpStatusCode StatusCode { get; }
public Version HttpVersion { get; }
public bool IsSuccessStatusCode { get; }
@@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public TestResponse(HttpStatusCode code, Stream response)
{
StatusCode = code;
HttpVersion = new Version(2, 0);
IsSuccessStatusCode = code == HttpStatusCode.OK;
_response = response;
}
+24 -22
View File
@@ -5,29 +5,31 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="11.3.0" />
<PackageReference Include="Bitfinex.Net" Version="9.3.0" />
<PackageReference Include="BitMart.Net" Version="2.4.1" />
<PackageReference Include="Bybit.Net" Version="5.4.0" />
<PackageReference Include="CoinEx.Net" Version="9.3.0" />
<PackageReference Include="CoinW.Net" Version="1.0.1" />
<PackageReference Include="CryptoCom.Net" Version="2.4.0" />
<PackageReference Include="DeepCoin.Net" Version="2.3.0" />
<PackageReference Include="GateIo.Net" Version="2.4.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.4.0" />
<PackageReference Include="JK.BingX.Net" Version="2.3.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.3.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.3.0" />
<PackageReference Include="JK.OKX.Net" Version="3.3.1" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.3.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.3.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.3.1" />
<PackageReference Include="Kucoin.Net" Version="7.3.0" />
<PackageReference Include="Binance.Net" Version="11.8.0" />
<PackageReference Include="Bitfinex.Net" Version="9.8.0" />
<PackageReference Include="BitMart.Net" Version="2.9.0" />
<PackageReference Include="BloFin.Net" Version="1.1.0" />
<PackageReference Include="Bybit.Net" Version="5.9.0" />
<PackageReference Include="CoinEx.Net" Version="9.8.0" />
<PackageReference Include="CoinW.Net" Version="1.5.0" />
<PackageReference Include="CryptoCom.Net" Version="2.9.0" />
<PackageReference Include="DeepCoin.Net" Version="2.8.0" />
<PackageReference Include="GateIo.Net" Version="2.9.1" />
<PackageReference Include="HyperLiquid.Net" Version="2.13.0" />
<PackageReference Include="JK.BingX.Net" Version="2.8.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.8.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.9.0" />
<PackageReference Include="JK.OKX.Net" Version="3.8.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="1.0.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.8.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.8.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.8.0" />
<PackageReference Include="Kucoin.Net" Version="7.8.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Toobit.Net" Version="1.2.1" />
<PackageReference Include="WhiteBit.Net" Version="2.4.0" />
<PackageReference Include="XT.Net" Version="2.3.1" />
<PackageReference Include="Toobit.Net" Version="1.7.0" />
<PackageReference Include="WhiteBit.Net" Version="2.9.0" />
<PackageReference Include="XT.Net" Version="2.8.0" />
</ItemGroup>
</Project>
+12 -2
View File
@@ -1,10 +1,12 @@
@page "/"
@inject IAsterRestClient asterClient
@inject IBinanceRestClient binanceClient
@inject IBingXRestClient bingXClient
@inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient
@inject IBitMartRestClient bitmartClient
@inject IBitMEXRestClient bitmexClient
@inject IBloFinRestClient bloFinClient
@inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient
@inject ICoinExRestClient coinexClient
@@ -33,12 +35,14 @@
protected override async Task OnInitializedAsync()
{
var asterTask = asterClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
@@ -47,7 +51,7 @@
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
var htxTask = htxClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync(); // HyperLiquid does not have BTC spot trading
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync();
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
@@ -56,7 +60,10 @@
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
if (asterTask.Result.Success)
_prices.Add("Aster", asterTask.Result.Data.LastPrice);
if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
@@ -76,6 +83,9 @@
if (bitmexTask.Result.Success)
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
if (bloFinTask.Result.Success)
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
if (bybitTask.Result.Success)
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
+8 -1
View File
@@ -1,10 +1,12 @@
@page "/LiveData"
@inject IAsterSocketClient asterSocketClient
@inject IBinanceSocketClient binanceSocketClient
@inject IBingXSocketClient bingXSocketClient
@inject IBitfinexSocketClient bitfinexSocketClient
@inject IBitgetSocketClient bitgetSocketClient
@inject IBitMartSocketClient bitmartSocketClient
@inject IBitMEXSocketClient bitmexSocketClient
@inject IBloFinSocketClient bloFinSocketClient
@inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient
@inject ICoinExSocketClient coinExSocketClient
@@ -42,12 +44,16 @@
{
var tasks = new Task<CallResult<UpdateSubscription>>[]
{
// Aster doesn't support the ETH/BTC pair
//asterSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Aster", data.Data.LastPrice)),
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
// BloFin doesn't support the ETH/BTC pair
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
@@ -61,7 +67,8 @@
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
// Mexc doesn't offer a ticker stream currently
//mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
// Toobit doesn't support the ETH/BTC pair
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
+10 -7
View File
@@ -1,12 +1,14 @@
@page "/OrderBooks"
@using System.Collections.Concurrent
@using System.Timers
@using Aster.Net.Interfaces
@using Binance.Net.Interfaces
@using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using BitMart.Net.Interfaces;
@using BitMEX.Net.Interfaces;
@using BloFin.Net.Interfaces;
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@@ -26,12 +28,14 @@
@using Toobit.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces
@inject IAsterOrderBookFactory asterFactory
@inject IBinanceOrderBookFactory binanceFactory
@inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBitgetOrderBookFactory bitgetFactory
@inject IBitMartOrderBookFactory bitmartFactory
@inject IBitMEXOrderBookFactory bitmexFactory
@inject IBloFinOrderBookFactory bloFinFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory
@inject ICoinExOrderBookFactory coinExFactory
@@ -55,7 +59,7 @@
@foreach(var book in _books.OrderBy(p => p.Key))
{
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
<h4>@book.Key</h4>
<h4>@book.Key - @book.Value.Symbol</h4>
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
{
for (var i = 0; i < 3; i++)
@@ -81,29 +85,28 @@
_books = new Dictionary<string, ISymbolOrderBook>
{
{ "Aster", binanceFactory.CreateSpot("ETHUSDT") },
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
// DeepCoin does not support the ETH/BTC pair
//{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
{ "DeepCoin", deepCoinFactory.Create("ETH-USDT") },
{ "HTX", htxFactory.CreateSpot("ethbtc") },
// HyperLiquid does not support the ETH/BTC pair
//{ "HyperLiquid", hyperLiquidFactory.Create("ETH/BTC") },
{ "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") },
// Toobit does not support the ETH/BTC pair
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
{ "XT", xtFactory.CreateSpot("eth_btc") },
};
+31 -24
View File
@@ -1,12 +1,14 @@
@page "/Trackers"
@using System.Collections.Concurrent
@using System.Timers
@using Aster.Net.Interfaces
@using Binance.Net.Interfaces
@using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using BitMEX.Net.Interfaces;
@using BitMart.Net.Interfaces;
@using BloFin.Net.Interfaces
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@@ -27,12 +29,14 @@
@using Toobit.Net.Interfaces;
@using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces
@inject IAsterTrackerFactory asterFactory
@inject IBinanceTrackerFactory binanceFactory
@inject IBingXTrackerFactory bingXFactory
@inject IBitfinexTrackerFactory bitfinexFactory
@inject IBitgetTrackerFactory bitgetFactory
@inject IBitMartTrackerFactory bitmartFactory
@inject IBitMEXTrackerFactory bitmexFactory
@inject IBloFinTrackerFactory bloFinFactory
@inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory
@inject ICoinExTrackerFactory coinExFactory
@@ -71,33 +75,36 @@
protected override async Task OnInitializedAsync()
{
var usdtSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var usdcSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDC");
var usdtSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var usdtPerpLinSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
_trackers = new List<ITradeTracker>
{
{ binanceFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ bingXFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitfinexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitgetFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitmartFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitmexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinWFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ htxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
// HyperLiquid doesn't support spot pair, but does have a futures BTC/USDC pair
{ hyperLiquidFactory.CreateTradeTracker(new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDC"), period: TimeSpan.FromMinutes(5)) },
{ krakenFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
{ asterFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitgetFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitmartFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitmexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bloFinFactory.CreateTradeTracker(usdtPerpLinSymbol, period: TimeSpan.FromMinutes(5)) },
{ bybitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinExFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinWFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ cryptocomFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ deepCoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ gateioFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ htxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ hyperLiquidFactory.CreateTradeTracker(usdcSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ krakenFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ kucoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ whitebitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
};
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
+2
View File
@@ -33,11 +33,13 @@ namespace BlazorClient
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
});
services.AddAster();
services.AddBingX();
services.AddBitfinex();
services.AddBitget();
services.AddBitMart();
services.AddBitMEX();
services.AddBloFin();
services.AddBybit();
services.AddCoinbase();
services.AddCoinEx();
+2
View File
@@ -8,12 +8,14 @@
@using Microsoft.JSInterop
@using BlazorClient
@using BlazorClient.Shared
@using Aster.Net.Interfaces.Clients;
@using Binance.Net.Interfaces.Clients;
@using BingX.Net.Interfaces.Clients;
@using Bitfinex.Net.Interfaces.Clients;
@using Bitget.Net.Interfaces.Clients;
@using BitMart.Net.Interfaces.Clients;
@using BitMEX.Net.Interfaces.Clients;
@using BloFin.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients;
@using Coinbase.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients;
+26
View File
@@ -12,12 +12,14 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|--|--|--|--|--|--|--|
|![Aster](https://raw.githubusercontent.com/JKorf/Aster.Net/refs/heads/main/Aster.Net/Icon/icon.png)|Aster|DEX|[JKorf/Aster.Net](https://github.com/JKorf/Aster.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Aster.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Aster.Net)|[Link](https://www.asterdex.com/en/referral/FD2E11)|4%|
|![Binance](https://raw.githubusercontent.com/JKorf/Binance.Net/refs/heads/master/Binance.Net/Icon/icon.png)|Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square)](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%|
|![BingX](https://raw.githubusercontent.com/JKorf/BingX.Net/refs/heads/main/BingX.Net/Icon/BingX.png)|BingX|CEX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.BingX.Net)|[Link](https://bingx.com/invite/FFHRJKWG/)|20%|
|![Bitfinex](https://raw.githubusercontent.com/JKorf/Bitfinex.Net/refs/heads/master/Bitfinex.Net/Icon/icon.png)|Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|-|-|
|![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-|
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|![CoinEx](https://raw.githubusercontent.com/JKorf/CoinEx.Net/refs/heads/master/CoinEx.Net/Icon/icon.png)|CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
@@ -38,6 +40,10 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
### Full demo application
A full demo application is available using the [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) library:
https://github.com/JKorf/CryptoManager.Net
## Discord
[![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](https://discord.gg/MSpeEtSY8t)
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
@@ -59,6 +65,26 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes
* Version 9.9.0 - 06 Oct 2025
* Updated socket Subscription status handling
* Added SubscriptionStatusChanged event to UpdateSubscription (SubscribeAsync methods reponse)
* Fixed timing issue for connection events in UpdateSubscription
* Version 9.8.0 - 30 Sep 2025
* Added ContractAddress to SharedAsset model
* Added ITrackerFactory interface
* Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed
* Fixed ExchangeHelpers.AdjustValueStep high precision calculation
* Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect
* Fixed EnumConverter to allow mapping empty string values
* Version 9.7.0 - 01 Sep 2025
* Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
* Added REST client option for selecting HTTP protocol version
* Added REST client option for HTTP client keep alive interval
* Added HttpVersion to WebCallResult responses
* Updated request logic to default to using HTTP version 2.0 for dotnet core
* Version 9.6.0 - 25 Aug 2025
* Added support for parsing REST response even though status indicates error
* Added better support for subscriptions without subscribe confirmation