1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-05 19:01:18 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Jkorf 4a4930d658 Updated to version 12.5.1 2026-09-01 09:03:50 +02:00
nils2525 ca245959a2 Fix subscription query completion for topic-filtered routes (#288) 2026-08-31 21:07:33 +02:00
Jkorf ff43d202d4 Add Tapbit to examples 2026-08-24 11:52:37 +02:00
Jkorf d1947e643b Added Tapbit reference 2026-08-24 10:55:38 +02:00
JKorf 6fec65cd6a Fixed failed ISymbolOrderBook start potentially leaving a pending background task 2026-08-23 20:36:47 +02:00
JKorf 053bcf6ecc Fixed caching issue for auth requests 2026-08-23 20:22:10 +02:00
JKorf 6957ba44f5 Fixed KlineTracker reporting incorrect High/Low price on GetStats result 2026-08-23 20:21:54 +02:00
14 changed files with 219 additions and 39 deletions
@@ -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;
}
}
}
+14 -1
View File
@@ -198,7 +198,7 @@ namespace CryptoExchange.Net.Clients
string? cacheKey = null; string? cacheKey = null;
if (ShouldCache(definition)) if (ShouldCache(definition))
{ {
cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData(); cacheKey = GetCacheKey(definition, uriParameters);
_logger.CheckingCache(cacheKey); _logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null) if (cachedValue != null)
@@ -800,6 +800,13 @@ namespace CryptoExchange.Net.Clients
&& definition.Method == HttpMethod.Get && definition.Method == HttpMethod.Get
&& !definition.PreventCaching; && !definition.PreventCaching;
/// <summary>
/// Produce a unique cache key for the request based on the request definition and parameters.
/// </summary>
protected virtual string GetCacheKey(RequestDefinition definition, Parameters? parameters)
{
return definition.FullUrl + definition + parameters?.ToFormData();
}
/// <inheritdoc /> /// <inheritdoc />
public virtual void SetOptions(UpdateOptions options) public virtual void SetOptions(UpdateOptions options)
@@ -889,6 +896,12 @@ namespace CryptoExchange.Net.Clients
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials; ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
} }
/// <inheritdoc />
protected override string GetCacheKey(RequestDefinition definition, Parameters? parameters)
{
return definition.FullUrl + definition + parameters?.ToFormData() + GetAuthenticationProvider()?.Key;
}
} }
/// <inheritdoc /> /// <inheritdoc />
+3 -3
View File
@@ -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>
@@ -289,6 +289,7 @@ namespace CryptoExchange.Net.OrderBook
if (!startResult.Success) if (!startResult.Success)
{ {
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
_queueEvent.Set();
return CallResult.Fail(startResult.Error!); return CallResult.Fail(startResult.Error!);
} }
@@ -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();
@@ -282,8 +282,8 @@ namespace CryptoExchange.Net.Trackers.Klines
KlineCount = klines.Count(), KlineCount = klines.Count(),
FirstOpenTime = klines.First().OpenTime, FirstOpenTime = klines.First().OpenTime,
LastOpenTime = klines.Last().OpenTime, LastOpenTime = klines.Last().OpenTime,
HighPrice = klines.Select(d => d.LowPrice).Max(), HighPrice = klines.Select(d => d.HighPrice).Max(),
LowPrice = klines.Select(d => d.HighPrice).Min(), LowPrice = klines.Select(d => d.LowPrice).Min(),
#pragma warning disable CS0618 // Type or member is obsolete | Temporary to maintain previous behavior #pragma warning disable CS0618 // Type or member is obsolete | Temporary to maintain previous behavior
Volume = klines.Select(d => d.Volume).Sum(), Volume = klines.Select(d => d.Volume).Sum(),
AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8) AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8)
+30 -29
View File
@@ -5,36 +5,37 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="13.3.0" /> <PackageReference Include="Binance.Net" Version="13.5.0" />
<PackageReference Include="Bitfinex.Net" Version="11.3.0" /> <PackageReference Include="Bitfinex.Net" Version="11.4.0" />
<PackageReference Include="BitMart.Net" Version="4.3.0" /> <PackageReference Include="BitMart.Net" Version="4.4.0" />
<PackageReference Include="BloFin.Net" Version="3.3.0" /> <PackageReference Include="BloFin.Net" Version="3.4.0" />
<PackageReference Include="Bybit.Net" Version="7.3.0" /> <PackageReference Include="Bybit.Net" Version="7.5.0" />
<PackageReference Include="CoinEx.Net" Version="11.3.0" /> <PackageReference Include="CoinEx.Net" Version="11.4.0" />
<PackageReference Include="CoinW.Net" Version="3.3.0" /> <PackageReference Include="CoinW.Net" Version="3.4.0" />
<PackageReference Include="CryptoCom.Net" Version="4.3.0" /> <PackageReference Include="CryptoCom.Net" Version="4.4.0" />
<PackageReference Include="DeepCoin.Net" Version="4.3.0" /> <PackageReference Include="DeepCoin.Net" Version="4.4.0" />
<PackageReference Include="GateIo.Net" Version="4.4.0" /> <PackageReference Include="GateIo.Net" Version="4.6.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.4.0" /> <PackageReference Include="HyperLiquid.Net" Version="5.7.0" />
<PackageReference Include="JK.BingX.Net" Version="4.3.0" /> <PackageReference Include="JK.BingX.Net" Version="4.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.3.2" /> <PackageReference Include="JK.Bitget.Net" Version="4.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="6.4.0" /> <PackageReference Include="JK.Mexc.Net" Version="6.6.0" />
<PackageReference Include="JK.OKX.Net" Version="5.3.1" /> <PackageReference Include="JK.OKX.Net" Version="5.4.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.3.0" /> <PackageReference Include="Jkorf.Aster.Net" Version="4.4.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.3.0" /> <PackageReference Include="JKorf.BitMEX.Net" Version="4.4.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.4.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="4.5.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.3.0" /> <PackageReference Include="JKorf.HTX.Net" Version="9.5.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.4.0" /> <PackageReference Include="JKorf.Lighter.Net" Version="1.5.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.3.0" /> <PackageReference Include="JKorf.Upbit.Net" Version="3.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.3.0" /> <PackageReference Include="KrakenExchange.Net" Version="8.4.0" />
<PackageReference Include="Kucoin.Net" Version="9.3.0" /> <PackageReference Include="Kucoin.Net" Version="9.5.0" />
<PackageReference Include="LBank.Net" Version="1.0.0" /> <PackageReference Include="LBank.Net" Version="1.1.0" />
<PackageReference Include="Pionex.Net" Version="1.1.0" /> <PackageReference Include="Pionex.Net" Version="1.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="4.3.0" /> <PackageReference Include="Tapbit.Net" Version="1.0.0" />
<PackageReference Include="Weex.Net" Version="2.3.0" /> <PackageReference Include="Toobit.Net" Version="4.4.0" />
<PackageReference Include="WhiteBit.Net" Version="4.3.0" /> <PackageReference Include="Weex.Net" Version="2.4.0" />
<PackageReference Include="XT.Net" Version="4.3.0" /> <PackageReference Include="WhiteBit.Net" Version="4.4.0" />
<PackageReference Include="XT.Net" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -1
View File
@@ -23,6 +23,7 @@
@inject IMexcRestClient mexcClient @inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IPionexRestClient pionexClient @inject IPionexRestClient pionexClient
@inject ITapbitRestClient tapbitClient
@inject IToobitRestClient toobitClient @inject IToobitRestClient toobitClient
@inject IUpbitRestClient upbitClient @inject IUpbitRestClient upbitClient
@inject IWeexRestClient weexClient @inject IWeexRestClient weexClient
@@ -70,7 +71,8 @@
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync(); var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt"); var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask); await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask,
htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask, toobitTask, upbitTask, weexTask, whitebitTask, xtTask);
if (asterTask.Result.Success) if (asterTask.Result.Success)
_prices.Add("Aster", asterTask.Result.Data.LastPrice); _prices.Add("Aster", asterTask.Result.Data.LastPrice);
+1
View File
@@ -57,6 +57,7 @@ namespace BlazorClient
services.AddMexc(); services.AddMexc();
services.AddOKX(); services.AddOKX();
services.AddPionex(); services.AddPionex();
services.AddTapbit();
services.AddToobit(); services.AddToobit();
services.AddUpbit(); services.AddUpbit();
services.AddWeex(); services.AddWeex();
+1
View File
@@ -33,6 +33,7 @@
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using Pionex.Net.Interfaces.Clients; @using Pionex.Net.Interfaces.Clients;
@using Upbit.Net.Interfaces.Clients; @using Upbit.Net.Interfaces.Clients;
@using Tapbit.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients; @using Toobit.Net.Interfaces.Clients;
@using Weex.Net.Interfaces.Clients @using Weex.Net.Interfaces.Clients
@using WhiteBit.Net.Interfaces.Clients @using WhiteBit.Net.Interfaces.Clients
+7
View File
@@ -59,6 +59,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%| |![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|![Pionex](https://raw.githubusercontent.com/JKorf/Pionex.Net/refs/heads/main/Pionex.Net/Icon/icon.png)|Pionex|CEX|[JKorf/Pionex.Net](https://github.com/JKorf/Pionex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Pionex.net.svg?style=flat-square)](https://www.nuget.org/packages/Pionex.Net)|-|-| |![Pionex](https://raw.githubusercontent.com/JKorf/Pionex.Net/refs/heads/main/Pionex.Net/Icon/icon.png)|Pionex|CEX|[JKorf/Pionex.Net](https://github.com/JKorf/Pionex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Pionex.net.svg?style=flat-square)](https://www.nuget.org/packages/Pionex.Net)|-|-|
|![Polymarket](https://raw.githubusercontent.com/JKorf/Polymarket.Net/main/Polymarket.Net/Icon/icon.png)|Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[![Nuget version](https://img.shields.io/nuget/v/Polymarket.net.svg?style=flat-square)](https://www.nuget.org/packages/Polymarket.Net)|-|-| |![Polymarket](https://raw.githubusercontent.com/JKorf/Polymarket.Net/main/Polymarket.Net/Icon/icon.png)|Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[![Nuget version](https://img.shields.io/nuget/v/Polymarket.net.svg?style=flat-square)](https://www.nuget.org/packages/Polymarket.Net)|-|-|
|![Tapbit](https://raw.githubusercontent.com/JKorf/Tapbit.Net/refs/heads/main/Tapbit.Net/Icon/icon.png)|Tapbit|CEX|[JKorf/Tapbit.Net](https://github.com/JKorf/Tapbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Tapbit.net.svg?style=flat-square)](https://www.nuget.org/packages/Tapbit.Net)|[Link](https://www.tapbit.com/en/invite/SPBGMSK)|-|
|![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-| |![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-| |![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|![Weex](https://raw.githubusercontent.com/JKorf/Weex.Net/refs/heads/main/Weex.Net/Icon/icon.png)|Weex|CEX|[JKorf/Weex.Net](https://github.com/JKorf/Weex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Weex.net.svg?style=flat-square)](https://www.nuget.org/packages/Weex.Net)|-|-| |![Weex](https://raw.githubusercontent.com/JKorf/Weex.Net/refs/heads/main/Weex.Net/Icon/icon.png)|Weex|CEX|[JKorf/Weex.Net](https://github.com/JKorf/Weex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Weex.net.svg?style=flat-square)](https://www.nuget.org/packages/Weex.Net)|-|-|
@@ -127,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
+1 -1
View File
@@ -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.