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

Compare commits

...

11 Commits

11 changed files with 185 additions and 25 deletions
@@ -454,9 +454,6 @@ namespace CryptoExchange.Net.Clients
{ {
memoryStream.Position = 0; memoryStream.Position = 0;
originalData = await reader.ReadToEndAsync().ConfigureAwait(false); originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
if (_logger.IsEnabled(LogLevel.Trace))
_logger.RestApiReceivedResponse(request.RequestId, originalData);
} }
// Continue processing from the memory stream since the response stream is already read and we can't seek it // Continue processing from the memory stream since the response stream is already read and we can't seek it
@@ -802,7 +802,6 @@ namespace CryptoExchange.Net.Clients
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection); return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
} }
/// <summary> /// <summary>
/// Process an unhandled message /// Process an unhandled message
/// </summary> /// </summary>
@@ -811,6 +810,14 @@ namespace CryptoExchange.Net.Clients
{ {
} }
/// <summary>
/// Process an unhandled message
/// </summary>
/// <param name="connection">The socket connection</param>
/// <param name="typeIdentifier">The type as identified</param>
/// <param name="data">The data</param>
protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan<byte> data) => false;
/// <summary> /// <summary>
/// Process connect rate limited /// Process connect rate limited
/// </summary> /// </summary>
@@ -0,0 +1,30 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated string values
/// </summary>
public class CommaSplitStringConverter : JsonConverter<string[]>
{
/// <inheritdoc />
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value));
}
}
}
+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>10.2.0</PackageVersion> <PackageVersion>10.2.4</PackageVersion>
<AssemblyVersion>10.2.0</AssemblyVersion> <AssemblyVersion>10.2.4</AssemblyVersion>
<FileVersion>10.2.0</FileVersion> <FileVersion>10.2.4</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>
+30 -1
View File
@@ -74,8 +74,13 @@ namespace CryptoExchange.Net
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
foreach(var entry in (object[])parameter.Value) bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value)
{ {
if (!firstArrayValue)
uriString.Append('&');
firstArrayValue = false;
uriString.Append(parameter.Key); uriString.Append(parameter.Key);
uriString.Append("[]="); uriString.Append("[]=");
if (urlEncodeValues) if (urlEncodeValues)
@@ -86,8 +91,12 @@ namespace CryptoExchange.Net
} }
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value) foreach (var entry in (object[])parameter.Value)
{ {
if (!firstArrayValue)
uriString.Append('&');
firstArrayValue = false;
uriString.Append(parameter.Key); uriString.Append(parameter.Key);
uriString.Append("="); uriString.Append("=");
if (urlEncodeValues) if (urlEncodeValues)
@@ -607,6 +616,26 @@ namespace CryptoExchange.Net
return services; return services;
} }
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
public static byte[] HexStringToBytes(this string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
} }
} }
@@ -22,7 +22,6 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit; private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit; private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested; private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
private static readonly Action<ILogger, int?, string?, Exception?> _restApiReceivedResponse;
static RestApiClientLoggingExtensions() static RestApiClientLoggingExtensions()
{ {
@@ -91,11 +90,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(4012, "RestApiCancellationRequested"), new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user"); "[Req {RequestId}] Request cancelled by user");
_restApiReceivedResponse = LoggerMessage.Define<int?, string?>(
LogLevel.Trace,
new EventId(4013, "RestApiReceivedResponse"),
"[Req {RequestId}] Received response: {Data}");
} }
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception) public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
@@ -161,10 +155,5 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_restApiCancellationRequested(logger, requestId, null); _restApiCancellationRequested(logger, requestId, null);
} }
public static void RestApiReceivedResponse(this ILogger logger, int requestId, string? originalData)
{
_restApiReceivedResponse(logger, requestId, originalData, null);
}
} }
} }
@@ -96,6 +96,23 @@ namespace CryptoExchange.Net.Objects
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
} }
/// <summary>
/// Add a DateTime value as string
/// </summary>
public void AddString(string key, DateTime value)
{
base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a DateTime value as string. Not added if value is null
/// </summary>
public void AddOptionalString(string key, DateTime? value)
{
if (value != null)
base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary> /// <summary>
/// Add a datetime value as milliseconds timestamp /// Add a datetime value as milliseconds timestamp
/// </summary> /// </summary>
@@ -242,6 +259,45 @@ namespace CryptoExchange.Net.Objects
} }
} }
/// <summary>
/// Add key as comma separated values
/// </summary>
public void AddCommaSeparated(string key, IEnumerable<string> values)
{
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as boolean lower case value
/// </summary>
public void AddBoolString(string key, bool value)
{
base.Add(key, value.ToString().ToLower());
}
/// <summary>
/// Add key as boolean lower case value if it's not null
/// </summary>
public void AddOptionalBoolString(string key, bool? value)
{
if (value == null)
return;
base.Add(key, value.ToString()!.ToLower());
}
/// <summary> /// <summary>
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object /// Set the request body. Can be used to specify a simple value or array as the body instead of an object
/// </summary> /// </summary>
@@ -640,6 +640,34 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(true); return new CallResult<bool>(true);
} }
/// <summary>
/// Wait until an update has been buffered
/// </summary>
/// <param name="timeout">Max wait time</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitUntilFirstUpdateBufferedAsync(TimeSpan timeout, CancellationToken ct)
{
var startWait = DateTime.UtcNow;
while (_processBuffer.Count == 0)
{
if (ct.IsCancellationRequested)
return new CallResult<bool>(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > timeout)
return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
await Task.Delay(20, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{ }
}
return new CallResult<bool>(true);
}
/// <summary> /// <summary>
/// IDisposable implementation for the order book /// IDisposable implementation for the order book
/// </summary> /// </summary>
@@ -1002,7 +1030,8 @@ namespace CryptoExchange.Net.OrderBook
private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber) private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber)
{ {
if (sequenceNumber < LastSequenceNumber) if (sequenceNumber < LastSequenceNumber
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
// Update is somehow from before the current state // Update is somehow from before the current state
return SequenceNumberResult.OutOfSync; return SequenceNumberResult.OutOfSync;
@@ -571,8 +571,12 @@ namespace CryptoExchange.Net.Sockets.Default
if (deserializationType == null) if (deserializationType == null)
{ {
// No handler found for identifier either, can't process if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
_logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray())); {
// No handler found for identifier either, can't process
_logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray()));
}
return; return;
} }
@@ -1291,8 +1295,9 @@ namespace CryptoExchange.Net.Sockets.Default
public void UpdateSequenceNumber(long sequenceNumber) public void UpdateSequenceNumber(long sequenceNumber)
{ {
if (ApiClient.EnforceSequenceNumbers if (ApiClient.EnforceSequenceNumbers
&& _lastSequenceNumber != 0 && _lastSequenceNumber != 0 // Initial value is 0
&& _lastSequenceNumber + 1 != sequenceNumber) && _lastSequenceNumber != sequenceNumber // When there are multiple listeners for the same message it's possible this gets recorded multiple times, shouldn't be an issue
&& _lastSequenceNumber + 1 != sequenceNumber) // Expected value
{ {
// Not sequential // Not sequential
_logger.LogWarning("[Sckt {SocketId}] update not in sequence. Last recorded sequence number: {LastSequence}, update sequence number: {UpdateSequence}. Reconnecting", SocketId, _lastSequenceNumber, sequenceNumber); _logger.LogWarning("[Sckt {SocketId}] update not in sequence. Last recorded sequence number: {LastSequence}, update sequence number: {UpdateSequence}. Reconnecting", SocketId, _lastSequenceNumber, sequenceNumber);
+2 -1
View File
@@ -113,7 +113,8 @@ namespace CryptoExchange.Net
/// <param name="api"></param> /// <param name="api"></param>
internal static void RegisterRestApi(string api) internal static void RegisterRestApi(string api)
{ {
_lastRestDelays[api] = new RestTimeOffset(); if (!_lastRestDelays.ContainsKey(api))
_lastRestDelays.TryAdd(api, new RestTimeOffset());
} }
/// <summary> /// <summary>
+17
View File
@@ -66,6 +66,23 @@ 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). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 10.2.4 - 17 Jan 2026
* Added WaitUntilFirstUpdateBufferedAsync method on SymbolOrderBook
* Added some util methods
* Added CommaSplitStringConverter
* Fixed sequence validation bug SymbolOrderBook
* Version 10.2.3 - 14 Jan 2026
* Added HandleUnhandledMessage virtual method to SocketApiClient to allow some processing for messages which couldn't be mapped via the normal way
* Fixed semaphore exception when creating a new REST client while time sync is in progress on another client
* Version 10.2.2 - 13 Jan 2026
* Allow the same websocket connection sequence number to be recorded multiple times
* Version 10.2.1 - 13 Jan 2026
* Removed duplicate logging for rest responses in Trace verbosity
* Fixed parameter URL creation for array values with ArrayParametersSerialization.MultipleValues
* Version 10.2.0 - 12 Jan 2026 * Version 10.2.0 - 12 Jan 2026
* Added EnforceSequenceNumbers property on SocketApiClient to configure whether websocket message contain sequence numbers and if these should be checked to be sequential * Added EnforceSequenceNumbers property on SocketApiClient to configure whether websocket message contain sequence numbers and if these should be checked to be sequential
* Added fallback to existing websocket connection if no dedicated request connection was found * Added fallback to existing websocket connection if no dedicated request connection was found