mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 00:43:03 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc8faf9822 | |||
| 90c1b89ceb | |||
| 21206ffb25 | |||
| 5942423bfb | |||
| dc4abc42a7 | |||
| c71a81e686 | |||
| 550c0eabf1 | |||
| 28a2a0c7fd | |||
| 7dd1cd5bbd |
@@ -802,7 +802,6 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Process an unhandled message
|
||||
/// </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>
|
||||
/// Process connect rate limited
|
||||
/// </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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>10.2.2</PackageVersion>
|
||||
<AssemblyVersion>10.2.2</AssemblyVersion>
|
||||
<FileVersion>10.2.2</FileVersion>
|
||||
<PackageVersion>10.2.5</PackageVersion>
|
||||
<AssemblyVersion>10.2.5</AssemblyVersion>
|
||||
<FileVersion>10.2.5</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>
|
||||
|
||||
@@ -74,8 +74,13 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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("[]=");
|
||||
if (urlEncodeValues)
|
||||
@@ -611,6 +616,26 @@ namespace CryptoExchange.Net
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,23 @@ namespace CryptoExchange.Net.Objects
|
||||
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>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
@@ -241,6 +258,45 @@ namespace CryptoExchange.Net.Objects
|
||||
base.Add(key, int.Parse(stringVal));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
|
||||
|
||||
@@ -640,6 +640,42 @@ namespace CryptoExchange.Net.OrderBook
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait until an update has been buffered
|
||||
/// </summary>
|
||||
/// <param name="minWait">Min wait time</param>
|
||||
/// <param name="maxWait">Max wait time</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult> WaitUntilFirstUpdateBufferedAsync(TimeSpan? minWait, TimeSpan maxWait, CancellationToken ct)
|
||||
{
|
||||
var startWait = DateTime.UtcNow;
|
||||
while (_processBuffer.Count == 0)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult(new CancellationRequestedError());
|
||||
|
||||
if (DateTime.UtcNow - startWait > maxWait)
|
||||
return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(20, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
}
|
||||
|
||||
if (minWait != null)
|
||||
{
|
||||
var dif = DateTime.UtcNow - startWait;
|
||||
if (dif < minWait)
|
||||
await Task.Delay(minWait.Value - dif).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IDisposable implementation for the order book
|
||||
/// </summary>
|
||||
@@ -1002,7 +1038,8 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber)
|
||||
{
|
||||
if (sequenceNumber < LastSequenceNumber)
|
||||
if (sequenceNumber < LastSequenceNumber
|
||||
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
|
||||
// Update is somehow from before the current state
|
||||
return SequenceNumberResult.OutOfSync;
|
||||
|
||||
|
||||
@@ -571,8 +571,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
|
||||
if (deserializationType == null)
|
||||
{
|
||||
// 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()));
|
||||
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -113,7 +115,8 @@ namespace CryptoExchange.Net
|
||||
/// <param name="api"></param>
|
||||
internal static void RegisterRestApi(string api)
|
||||
{
|
||||
_lastRestDelays[api] = new RestTimeOffset();
|
||||
if (!_lastRestDelays.ContainsKey(api))
|
||||
_lastRestDelays.TryAdd(api, new RestTimeOffset());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -138,6 +141,26 @@ namespace CryptoExchange.Net
|
||||
/// <param name="api">API name</param>
|
||||
public static TimeSpan? GetSocketOffset(string api) => _lastSocketDelays.TryGetValue(api, out var val) && val.Offset != null ? TimeSpan.FromMilliseconds(val.Offset.Value) : null;
|
||||
|
||||
/// <summary>
|
||||
/// Get a dictionary of API/Client name -> time offset for Rest api's
|
||||
/// </summary>
|
||||
public static Dictionary<string, TimeSpan?> GetRestOffsets()
|
||||
{
|
||||
return _lastRestDelays.ToDictionary(
|
||||
x => x.Key,
|
||||
x => (x.Value.Offset == null ? (TimeSpan?)null : TimeSpan.FromMilliseconds(x.Value.Offset.Value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a dictionary of API/Client name -> time offset for Websocket api's
|
||||
/// </summary>
|
||||
public static Dictionary<string, TimeSpan?> GetWebsocketOffsets()
|
||||
{
|
||||
return _lastSocketDelays.ToDictionary(
|
||||
x => x.Key,
|
||||
x => (x.Value.Offset == null ? (TimeSpan?)null : TimeSpan.FromMilliseconds(x.Value.Offset.Value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the WebSocket API update timestamp to trigger a new time offset calculation
|
||||
/// </summary>
|
||||
|
||||
@@ -66,6 +66,20 @@ 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 10.2.5 - 19 Jan 2026
|
||||
* Updated SymbolOrderBook.WaitUntilFirstUpdateBufferedAsync
|
||||
* Added GetRestOffsets and GetWebsocketOffsets to TimeOffsetManager
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user