mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) 
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user