mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-13 09:23:04 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68067d6258 | |||
| b309deb0c4 | |||
| e1dafdf0dd | |||
| fd7b5f0f0f | |||
| 3b735d66fd | |||
| 3cd505ac8b | |||
| 81d856d78d | |||
| 11c1ad871a |
@@ -228,7 +228,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
uriParameters,
|
uriParameters,
|
||||||
bodyParameters,
|
bodyParameters,
|
||||||
additionalHeaders);
|
additionalHeaders);
|
||||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||||
TotalRequestsMade++;
|
TotalRequestsMade++;
|
||||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
|
|||||||
@@ -189,7 +189,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
if (subscription.Authenticated && AuthenticationProvider == null)
|
if (subscription.Authenticated && AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
@@ -786,9 +789,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Preprocess a stream message
|
/// Preprocess a stream message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="connection"></param>
|
||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||||
if (attribute == null)
|
if (attribute == null)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var targetType = attribute.TargetType;
|
var targetType = attribute.TargetType;
|
||||||
object? value = null;
|
object? value = null;
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read string or number as string
|
||||||
|
/// </summary>
|
||||||
|
public class NumberStringConverter : JsonConverter<string?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
return reader.GetInt64().ToString();
|
||||||
|
|
||||||
|
return reader.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>7.9.0</PackageVersion>
|
<PackageVersion>7.10.0</PackageVersion>
|
||||||
<AssemblyVersion>7.9.0</AssemblyVersion>
|
<AssemblyVersion>7.10.0</AssemblyVersion>
|
||||||
<FileVersion>7.9.0</FileVersion>
|
<FileVersion>7.10.0</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</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</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using Gzip
|
/// Decompress using GzipStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -467,6 +467,23 @@ namespace CryptoExchange.Net
|
|||||||
deflateStream.CopyTo(decompressedStream);
|
deflateStream.CopyTo(decompressedStream);
|
||||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decompress using DeflateStream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
||||||
|
{
|
||||||
|
var output = new MemoryStream();
|
||||||
|
|
||||||
|
using (var compressStream = new MemoryStream(input.ToArray()))
|
||||||
|
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
||||||
|
decompressor.CopyTo(output);
|
||||||
|
|
||||||
|
output.Position = 0;
|
||||||
|
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,27 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a datetime value as string seconds timestamp
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public void AddSecondsString(string key, DateTime value)
|
||||||
|
{
|
||||||
|
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a datetime value as string seconds timestamp. Not added if value is null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public void AddOptionalSecondsString(string key, DateTime? value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -167,7 +188,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public void AddEnumAsInt<T>(string key, T value)
|
public void AddEnumAsInt<T>(string key, T value)
|
||||||
{
|
{
|
||||||
var stringVal = EnumConverter.GetString(value);
|
var stringVal = EnumConverter.GetString(value);
|
||||||
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
|
Add(key, int.Parse(stringVal)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -810,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
if (lastUpdateId <= LastSequenceNumber)
|
if (lastUpdateId <= LastSequenceNumber)
|
||||||
{
|
{
|
||||||
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
|
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
string? originalData = null;
|
string? originalData = null;
|
||||||
|
|
||||||
// 1. Decrypt/Preprocess if necessary
|
// 1. Decrypt/Preprocess if necessary
|
||||||
data = ApiClient.PreprocessStreamMessage(type, data);
|
data = ApiClient.PreprocessStreamMessage(this, type, data);
|
||||||
|
|
||||||
// 2. Read data into accessor
|
// 2. Read data into accessor
|
||||||
_accessor.Read(data);
|
_accessor.Read(data);
|
||||||
@@ -854,31 +854,32 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger.AuthenticationSucceeded(SocketId);
|
_logger.AuthenticationSucceeded(SocketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a list of all subscriptions on the socket
|
|
||||||
List<Subscription> subList;
|
|
||||||
lock (_listenersLock)
|
|
||||||
subList = _listeners.OfType<Subscription>().ToList();
|
|
||||||
|
|
||||||
foreach(var subscription in subList)
|
|
||||||
{
|
|
||||||
subscription.ConnectionInvocations = 0;
|
|
||||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
||||||
for (var i = 0; i < subList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
int batch = 0;
|
||||||
|
int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
|
List<Subscription> subList;
|
||||||
|
lock (_listenersLock)
|
||||||
|
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
|
||||||
|
|
||||||
|
if (subList.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
var taskList = new List<Task<CallResult>>();
|
var taskList = new List<Task<CallResult>>();
|
||||||
foreach (var subscription in subList.Skip(i).Take(ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
foreach (var subscription in subList)
|
||||||
{
|
{
|
||||||
|
subscription.ConnectionInvocations = 0;
|
||||||
|
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
var subQuery = subscription.GetSubQuery(this);
|
var subQuery = subscription.GetSubQuery(this);
|
||||||
if (subQuery == null)
|
if (subQuery == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -897,6 +898,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||||
if (taskList.Any(t => !t.Result.Success))
|
if (taskList.Any(t => !t.Result.Success))
|
||||||
return taskList.First(t => !t.Result.Success).Result;
|
return taskList.First(t => !t.Result.Success).Result;
|
||||||
|
|
||||||
|
batch++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for executing REST API integration tests
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient">Client type</typeparam>
|
||||||
|
public abstract class RestIntergrationTest<TClient>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get a client instance
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract TClient GetClient(ILoggerFactory loggerFactory);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool Run { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation.
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a client
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected TClient CreateClient()
|
||||||
|
{
|
||||||
|
var fact = new LoggerFactory();
|
||||||
|
fact.AddProvider(new TraceLoggerProvider());
|
||||||
|
return GetClient(fact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if integration tests should be executed
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected bool ShouldRun()
|
||||||
|
{
|
||||||
|
var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION");
|
||||||
|
if (!Run && integrationTests != "1")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a REST endpoint call and check for any errors or warnings.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of response</typeparam>
|
||||||
|
/// <param name="expression">The call expression</param>
|
||||||
|
/// <param name="authRequest">Whether this is an authenticated request</param>
|
||||||
|
public async Task RunAndCheckResult<T>(Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest)
|
||||||
|
{
|
||||||
|
if (!ShouldRun())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var client = CreateClient();
|
||||||
|
|
||||||
|
var expressionBody = (MethodCallExpression)expression.Body;
|
||||||
|
if (authRequest && !Authenticated)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
WebCallResult<T> result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = await expression.Compile().Invoke(client).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
||||||
|
|
||||||
|
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# CryptoExchange.Net
|
#  CryptoExchange.Net
|
||||||
|
|
||||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) 
|
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net) 
|
||||||
|
|
||||||
@@ -46,6 +46,17 @@ 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 7.10.0 - 26 Jul 2024
|
||||||
|
* Added System.Text.Json NumberStringConverter
|
||||||
|
* Added integration testing base class
|
||||||
|
* Added AddSecondsString and AddOptionalSecondsString to ParameterCollection
|
||||||
|
* Added Decompress method for ReadOnlyMemory using non-GZip deflate
|
||||||
|
* Added SocketConnection parameter to SocketConnection PreprocessStreamMessage
|
||||||
|
* Fixed websocket reconnect/unsubscribe timing bug
|
||||||
|
* Fixed issue in System.Text.Json array object deserialization skipping property when skipping an index
|
||||||
|
* Fixed order book logging bug
|
||||||
|
* Fixed bug in ParameterCollection AddEnumAsInt
|
||||||
|
|
||||||
* Version 7.9.0 - 16 Jul 2024
|
* Version 7.9.0 - 16 Jul 2024
|
||||||
* Added some checks in websocket connection handling
|
* Added some checks in websocket connection handling
|
||||||
* Added As<T> and AsError<T> methods on untyped WebCallResult
|
* Added As<T> and AsError<T> methods on untyped WebCallResult
|
||||||
|
|||||||
+1
-1
@@ -1389,7 +1389,7 @@ await client.UnsubscribeAllAsync();</code></pre>
|
|||||||
============================ -->
|
============================ -->
|
||||||
<section id="idocs_common">
|
<section id="idocs_common">
|
||||||
<h2>Common Clients</h2>
|
<h2>Common Clients</h2>
|
||||||
<p>The CryptoClients.Net client exposes some common client classes. These clients aim to make using the different API's easier.</p>
|
<p>The CryptoClients.Net library exposes two client classes. These clients aim to make using the different API's easier.</p>
|
||||||
|
|
||||||
<p><b>(I)ExchangeRestClient</b><br />
|
<p><b>(I)ExchangeRestClient</b><br />
|
||||||
The <code>IExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.
|
The <code>IExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.
|
||||||
|
|||||||
Reference in New Issue
Block a user