mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-10-16 16:10:53 +00:00
Merge branch 'master' into feature/shared-apis-v2
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Routing;
|
||||||
|
using CryptoExchange.Net.UnitTests.Implementations;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SubscriptionTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_OnlyCompleteSubscriptionQuery_ForMatchingTopic()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicASubscription = new TopicSubscription("topic-a");
|
||||||
|
var topicBSubscription = new TopicSubscription("topic-b");
|
||||||
|
var topicAQuery = topicASubscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
var topicBQuery = topicBSubscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var topicAHandled = topicASubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
var topicBHandled = topicBSubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(topicAHandled, Is.True);
|
||||||
|
Assert.That(topicBHandled, Is.False);
|
||||||
|
Assert.That(topicAQuery.Completed, Is.True);
|
||||||
|
Assert.That(topicAQuery.Success, Is.True);
|
||||||
|
Assert.That(topicBQuery.Completed, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_CompleteSubscriptionQuery_BeforeInvokingMatchingHandler()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Query? query = null;
|
||||||
|
var queryCompletedWhenHandlerInvoked = false;
|
||||||
|
var subscription = new TopicSubscription("topic", () => queryCompletedWhenHandlerInvoked = query!.Completed);
|
||||||
|
query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(queryCompletedWhenHandlerInvoked, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(null)]
|
||||||
|
[TestCase("topic")]
|
||||||
|
public void Handle_Should_CompleteSubscriptionQuery_ForUnfilteredRoute(string? topicFilter)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var subscription = new TopicSubscription(topic: null);
|
||||||
|
var query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = subscription.Handle("type", topicFilter, null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(query.Completed, Is.True);
|
||||||
|
Assert.That(query.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_TreatEmptyTopicFilterAsUnfilteredRoute()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var subscription = new TopicSubscription(string.Empty);
|
||||||
|
var query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(query.Completed, Is.True);
|
||||||
|
Assert.That(query.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Handle_Should_CompleteSubscriptionQuery_ForAnyMatchingTopic()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var subscription = new TopicSubscription(["topic-a", "topic-b"]);
|
||||||
|
var query = subscription.CreateSubscriptionQuery(null!)!;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var handled = subscription.Handle("type", "topic-b", null!, DateTime.UtcNow, "original", "data");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(handled, Is.True);
|
||||||
|
Assert.That(query.Completed, Is.True);
|
||||||
|
Assert.That(query.Success, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TopicSubscription : Subscription
|
||||||
|
{
|
||||||
|
public TopicSubscription(string? topic, Action? handler = null)
|
||||||
|
: base(NullLogger.Instance, false)
|
||||||
|
{
|
||||||
|
MessageRouter = MessageRouter.CreateForEvent<string>("type", topic, (_, _, _, _) =>
|
||||||
|
{
|
||||||
|
handler?.Invoke();
|
||||||
|
return CallResult.Ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopicSubscription(string[] topics)
|
||||||
|
: base(NullLogger.Instance, false)
|
||||||
|
{
|
||||||
|
MessageRouter = MessageRouter.CreateForEvent<string>("type", topics, (_, _, _, _) => CallResult.Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Query? GetSubQuery(SocketConnection connection)
|
||||||
|
{
|
||||||
|
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false)
|
||||||
|
{
|
||||||
|
TimeoutBehavior = TimeoutBehavior.Succeed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>12.5.0</PackageVersion>
|
<PackageVersion>12.5.1</PackageVersion>
|
||||||
<AssemblyVersion>12.5.0</AssemblyVersion>
|
<AssemblyVersion>12.5.1</AssemblyVersion>
|
||||||
<FileVersion>12.5.0</FileVersion>
|
<FileVersion>12.5.1</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
|
|||||||
Routes = routes;
|
Routes = routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RouteCollection? GetRoutes(string typeIdentifier)
|
||||||
|
=> (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Build the route mapping
|
/// Build the route mapping
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -46,13 +49,21 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
|
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
|
||||||
{
|
{
|
||||||
var routeCollection = (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier);
|
var routeCollection = GetRoutes(typeIdentifier);
|
||||||
if (routeCollection == null)
|
if (routeCollection == null)
|
||||||
throw new InvalidOperationException($"No routes for {typeIdentifier} message type");
|
throw new InvalidOperationException($"No routes for {typeIdentifier} message type");
|
||||||
|
|
||||||
return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result);
|
return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the router has a matching route for the specified message type and topic
|
||||||
|
/// </summary>
|
||||||
|
public bool CanHandle(string typeIdentifier, string? topicFilter)
|
||||||
|
{
|
||||||
|
return GetRoutes(typeIdentifier)?.CanHandle(topicFilter) == true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a void handler
|
/// Create a void handler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
|
|||||||
return matchingTopicRoutes;
|
return matchingTopicRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(string? topicFilter)
|
||||||
|
{
|
||||||
|
if (_routesWithoutTopicFilter.Count > 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return topicFilter != null
|
||||||
|
&& GetRoutesWithMatchingTopicFilter(topicFilter) != null;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result);
|
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,9 +219,13 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
ConnectionInvocations++;
|
ConnectionInvocations++;
|
||||||
TotalInvocations++;
|
TotalInvocations++;
|
||||||
|
|
||||||
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed)
|
if (SubscriptionQuery != null
|
||||||
|
&& !SubscriptionQuery.Completed
|
||||||
|
&& SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed
|
||||||
|
&& MessageRouter.CanHandle(typeIdentifier, topicFilter))
|
||||||
{
|
{
|
||||||
// The subscription query is one where it is successful if there is no error returned
|
// The subscription query is one where it is successful if there is no error returned
|
||||||
|
// The connection routes by type, so only a matching subscription route proves the query was successful
|
||||||
// Since we've received a data update for the subscription we can assume the subscribe query was successful
|
// Since we've received a data update for the subscription we can assume the subscribe query was successful
|
||||||
// Call timeout to complete
|
// Call timeout to complete
|
||||||
SubscriptionQuery.Timeout();
|
SubscriptionQuery.Timeout();
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ Various:
|
|||||||
* PlatformInfo now required support environment names in the constructor
|
* PlatformInfo now required support environment names in the constructor
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 12.5.1 - 01 Sep 2026
|
||||||
|
* Fixed KlineTracker reporting incorrect High/Low price on GetStats result
|
||||||
|
* Fixed caching issue for auth requests
|
||||||
|
* Fixed failed ISymbolOrderBook start potentially leaving a pending background task
|
||||||
|
* Fixed subscription query completion potentially reporting success too early for topic-filtered routes
|
||||||
|
|
||||||
* Version 12.5.0 - 21 Aug 2026
|
* Version 12.5.0 - 21 Aug 2026
|
||||||
* Shared APIs
|
* Shared APIs
|
||||||
* Added ISpotOrderManagementSocketClient for placing/canceling Spot orders via Shared websocket implementation
|
* Added ISpotOrderManagementSocketClient for placing/canceling Spot orders via Shared websocket implementation
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
|
> Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
|
||||||
|
|
||||||
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
|
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.1. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
|
||||||
|
|
||||||
The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.
|
The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user