1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

8 Commits

12 changed files with 219 additions and 29 deletions
+1 -1
View File
@@ -228,7 +228,7 @@ namespace CryptoExchange.Net.Clients
uriParameters,
bodyParameters,
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++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (!result)
@@ -189,7 +189,10 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null)
{
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
}
SocketConnection socketConnection;
var released = false;
@@ -786,9 +789,10 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Preprocess a stream message
/// </summary>
/// <param name="connection"></param>
/// <param name="type"></param>
/// <param name="data"></param>
/// <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);
if (attribute == null)
{
index++;
continue;
}
var targetType = attribute.TargetType;
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);
}
}
}
+3 -3
View File
@@ -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>7.9.0</PackageVersion>
<AssemblyVersion>7.9.0</AssemblyVersion>
<FileVersion>7.9.0</FileVersion>
<PackageVersion>7.10.0</PackageVersion>
<AssemblyVersion>7.10.0</AssemblyVersion>
<FileVersion>7.10.0</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</PackageTags>
<RepositoryType>git</RepositoryType>
+18 -1
View File
@@ -453,7 +453,7 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Decompress using Gzip
/// Decompress using GzipStream
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
@@ -467,6 +467,23 @@ namespace CryptoExchange.Net
deflateStream.CopyTo(decompressedStream);
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));
}
/// <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>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
@@ -167,7 +188,7 @@ namespace CryptoExchange.Net.Objects
public void AddEnumAsInt<T>(string key, T value)
{
var stringVal = EnumConverter.GetString(value);
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
Add(key, int.Parse(stringVal)!);
}
/// <summary>
@@ -810,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
{
if (lastUpdateId <= LastSequenceNumber)
{
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
return;
}
+22 -19
View File
@@ -420,7 +420,7 @@ namespace CryptoExchange.Net.Sockets
string? originalData = null;
// 1. Decrypt/Preprocess if necessary
data = ApiClient.PreprocessStreamMessage(type, data);
data = ApiClient.PreprocessStreamMessage(this, type, data);
// 2. Read data into accessor
_accessor.Read(data);
@@ -854,31 +854,32 @@ namespace CryptoExchange.Net.Sockets
_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
for (var i = 0; i < subList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
int batch = 0;
int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket;
while (true)
{
if (!_socket.IsOpen)
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>>();
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);
if (subQuery == null)
continue;
@@ -897,6 +898,8 @@ namespace CryptoExchange.Net.Sockets
await Task.WhenAll(taskList).ConfigureAwait(false);
if (taskList.Any(t => !t.Result.Success))
return taskList.First(t => !t.Result.Success).Result;
batch++;
}
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}");
}
}
}
+12 -1
View File
@@ -1,4 +1,4 @@
# CryptoExchange.Net
# ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
@@ -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).
## 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
* Added some checks in websocket connection handling
* Added As<T> and AsError<T> methods on untyped WebCallResult
+1 -1
View File
@@ -1389,7 +1389,7 @@ await client.UnsubscribeAllAsync();</code></pre>
============================ -->
<section id="idocs_common">
<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 />
The <code>IExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.