mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ab85d4c26 | |||
| be68115099 | |||
| ff0550b0fb | |||
| 1ab1e008fc | |||
| 6f30c72608 | |||
| e927bc3d20 | |||
| 09ed7d1436 | |||
| 6fed657ea6 | |||
| 1555f8da0c | |||
| 68b28fc875 | |||
| 5d50d8cde8 | |||
| 9ff673d8be | |||
| 3e5a34fb56 | |||
| 64ee50d98c | |||
| 6a105c6f8f | |||
| 287aadc720 | |||
| 7229438a0b | |||
| 444af98a15 | |||
| 70c6fa1bbb | |||
| d27f394b46 | |||
| c8c98e13d0 | |||
| 9fcd722991 | |||
| 8080ecccc0 | |||
| 4b6fa9a1b1 | |||
| 0b6dbde7d4 | |||
| fe4d63ba75 | |||
| 04bd3727ca | |||
| 7e6fcd03c2 | |||
| fde8d6353b | |||
| 41b996168a | |||
| b26f8fb900 | |||
| bdbbc61d86 | |||
| d64e200f2f | |||
| 71d54e2f9a | |||
| ba3975993f | |||
| 050286ecd1 | |||
| 96c9a55c48 | |||
| a20cbb2f1c | |||
| 2e957d7d9e | |||
| 18d0341056 | |||
| a2bfed2433 | |||
| 67299338a8 | |||
| 971c049c5f |
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
result = messageEvent.Data;
|
result = messageEvent.Data;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
@@ -107,7 +106,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
return deserializeResult;
|
return deserializeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
public override TimeSpan? GetTimeOffset() => null;
|
public override TimeSpan? GetTimeOffset() => null;
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
@@ -66,11 +68,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, IDictionary<string, object> uriParams, IDictionary<string, object> bodyParams, Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
||||||
{
|
{
|
||||||
bodyParameters = new SortedDictionary<string, object>();
|
|
||||||
uriParameters = new SortedDictionary<string, object>();
|
|
||||||
headers = new Dictionary<string, string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKey() => _credentials.Key.GetString();
|
public string GetKey() => _credentials.Key.GetString();
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||||
@@ -178,6 +181,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
internal IWebsocket CreateSocketInternal(string address)
|
internal IWebsocket CreateSocketInternal(string address)
|
||||||
{
|
{
|
||||||
return CreateSocket(address);
|
return CreateSocket(address);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -15,10 +16,12 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AuthenticationProvider : IDisposable
|
public abstract class AuthenticationProvider : IDisposable
|
||||||
{
|
{
|
||||||
|
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided credentials
|
/// Provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly ApiCredentials _credentials;
|
protected internal readonly ApiCredentials _credentials;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Byte representation of the secret
|
/// Byte representation of the secret
|
||||||
@@ -44,26 +47,24 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="apiClient">The Api client sending the request</param>
|
/// <param name="apiClient">The Api client sending the request</param>
|
||||||
/// <param name="uri">The uri for the request</param>
|
/// <param name="uri">The uri for the request</param>
|
||||||
/// <param name="method">The method of the request</param>
|
/// <param name="method">The method of the request</param>
|
||||||
/// <param name="providedParameters">The request parameters</param>
|
|
||||||
/// <param name="auth">If the requests should be authenticated</param>
|
/// <param name="auth">If the requests should be authenticated</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
|
||||||
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
||||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
||||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||||
/// <param name="headers">The headers that should be send with the request</param>
|
/// <param name="headers">The headers that should be send with the request</param>
|
||||||
|
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||||
public abstract void AuthenticateRequest(
|
public abstract void AuthenticateRequest(
|
||||||
RestApiClient apiClient,
|
RestApiClient apiClient,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
Dictionary<string, object> providedParameters,
|
IDictionary<string, object> uriParameters,
|
||||||
|
IDictionary<string, object> bodyParameters,
|
||||||
|
Dictionary<string, string> headers,
|
||||||
bool auth,
|
bool auth,
|
||||||
ArrayParametersSerialization arraySerialization,
|
ArrayParametersSerialization arraySerialization,
|
||||||
HttpMethodParameterPosition parameterPosition,
|
HttpMethodParameterPosition parameterPosition,
|
||||||
RequestBodyFormat requestBodyFormat,
|
RequestBodyFormat requestBodyFormat
|
||||||
out SortedDictionary<string, object> uriParameters,
|
|
||||||
out SortedDictionary<string, object> bodyParameters,
|
|
||||||
out Dictionary<string, string> headers
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -418,9 +419,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
protected DateTime GetTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -428,11 +429,25 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return the serialized request body
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serializer"></param>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||||
|
{
|
||||||
|
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
|
||||||
|
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]);
|
||||||
|
else
|
||||||
|
return serializer.Serialize(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Caching
|
||||||
|
{
|
||||||
|
internal class MemoryCache
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="value">Cache value</param>
|
||||||
|
public void Add(string key, object value)
|
||||||
|
{
|
||||||
|
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||||
|
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a cached value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="maxAge">The max age of the cached entry</param>
|
||||||
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
|
public object? Get(string key, TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue(key, out CacheItem value);
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryRemove(key, out _);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CacheItem
|
||||||
|
{
|
||||||
|
public DateTime CacheTime { get; }
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public CacheItem(DateTime cacheTime, object value)
|
||||||
|
{
|
||||||
|
CacheTime = cacheTime;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
@@ -17,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
|
|||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Requests;
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -40,23 +42,33 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request body content type
|
/// Request body content type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How to serialize array parameters when making requests
|
/// How to serialize array parameters when making requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string RequestBodyEmptyContent = "{}";
|
protected internal string RequestBodyEmptyContent = "{}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request headers to be sent with each request
|
/// Request headers to be sent with each request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether parameters need to be ordered
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool OrderParameters { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parameter order comparer
|
||||||
|
/// </summary>
|
||||||
|
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Where to put the parameters for requests with different Http methods
|
/// Where to put the parameters for requests with different Http methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -65,7 +77,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
||||||
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
||||||
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
||||||
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
|
{ HttpMethod.Put, HttpMethodParameterPosition.InBody },
|
||||||
|
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -74,6 +87,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory cache
|
||||||
|
/// </summary>
|
||||||
|
private static MemoryCache _cache = new MemoryCache();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -139,7 +156,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
ParameterCollection? parameters,
|
||||||
@@ -147,15 +164,67 @@ namespace CryptoExchange.Net.Clients
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null) where T : class
|
int? weight = null) where T : class
|
||||||
{
|
{
|
||||||
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
|
return SendAsync<T>(
|
||||||
|
baseAddress,
|
||||||
|
definition,
|
||||||
|
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
||||||
|
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||||
|
cancellationToken,
|
||||||
|
additionalHeaders,
|
||||||
|
weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a request to the base address based on the request definition
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Response type</typeparam>
|
||||||
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="uriParameters">Request query parameters</param>
|
||||||
|
/// <param name="bodyParameters">Request body parameters</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||||
|
string baseAddress,
|
||||||
|
RequestDefinition definition,
|
||||||
|
ParameterCollection? uriParameters,
|
||||||
|
ParameterCollection? bodyParameters,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
int? weight = null) where T : class
|
||||||
|
{
|
||||||
|
var key = baseAddress + definition + uriParameters?.ToFormData();
|
||||||
|
if (ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
currentTry++;
|
||||||
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
var prepareResult = await PrepareAsync(baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||||
if (!prepareResult)
|
if (!prepareResult)
|
||||||
return new WebCallResult<T>(prepareResult.Error!);
|
return new WebCallResult<T>(prepareResult.Error!);
|
||||||
|
|
||||||
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders);
|
var request = CreateRequest(
|
||||||
|
baseAddress,
|
||||||
|
definition,
|
||||||
|
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, 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);
|
||||||
@@ -167,6 +236,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +251,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="parameters">Request parameters</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
/// <param name="weight">Override the request weight for this request</param>
|
/// <param name="weight">Override the request weight for this request</param>
|
||||||
@@ -185,7 +259,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected virtual async Task<CallResult> PrepareAsync(
|
protected virtual async Task<CallResult> PrepareAsync(
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null)
|
int? weight = null)
|
||||||
@@ -225,7 +298,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult(limitResult.Error!);
|
return new CallResult(limitResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -239,7 +312,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult(limitResult.Error!);
|
return new CallResult(limitResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -253,38 +326,27 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
/// <param name="uriParameters">The query parameters of the request</param>
|
||||||
|
/// <param name="bodyParameters">The body parameters of the request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IRequest CreateRequest(
|
protected virtual IRequest CreateRequest(
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
ParameterCollection? uriParameters,
|
||||||
|
ParameterCollection? bodyParameters,
|
||||||
Dictionary<string, string>? additionalHeaders)
|
Dictionary<string, string>? additionalHeaders)
|
||||||
{
|
{
|
||||||
parameters ??= new ParameterCollection();
|
var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters);
|
||||||
|
var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters);
|
||||||
|
|
||||||
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
|
||||||
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
||||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||||
var requestId = ExchangeHelpers.NextId();
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
for (var i = 0; i < parameters.Count; i++)
|
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> delegateValue)
|
|
||||||
parameters[kvp.Key] = delegateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
|
||||||
{
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var headers = new Dictionary<string, string>();
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
if (AuthenticationProvider != null)
|
if (AuthenticationProvider != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -293,14 +355,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
this,
|
this,
|
||||||
uri,
|
uri,
|
||||||
definition.Method,
|
definition.Method,
|
||||||
parameters,
|
uriParams,
|
||||||
|
bodyParams,
|
||||||
|
headers,
|
||||||
definition.Authenticated,
|
definition.Authenticated,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
bodyFormat,
|
bodyFormat
|
||||||
out uriParameters,
|
);
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -308,18 +370,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
foreach (var param in parameters)
|
|
||||||
{
|
|
||||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
|
||||||
{
|
|
||||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
|
||||||
$"should return provided parameters in either the uri or body parameters output");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
uri = uri.SetParameters(uriParams, arraySerialization);
|
||||||
|
|
||||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
||||||
request.Accept = Constants.JsonContentHeader;
|
request.Accept = Constants.JsonContentHeader;
|
||||||
@@ -346,8 +398,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
{
|
{
|
||||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
if (bodyParameters.Any())
|
if (bodyParams.Count != 0)
|
||||||
WriteParamBody(request, bodyParameters, contentType);
|
WriteParamBody(request, bodyParams, contentType);
|
||||||
else
|
else
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
}
|
}
|
||||||
@@ -420,6 +472,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
/// <param name="gate">The ratelimit gate to use</param>
|
||||||
|
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNull]
|
[return: NotNull]
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
@@ -433,9 +486,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
IRateLimitGate? gate = null
|
IRateLimitGate? gate = null,
|
||||||
|
bool preventCaching = false
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
|
var key = uri.ToString() + method + signed + parameters?.ToFormData();
|
||||||
|
if (ShouldCache(method) && !preventCaching)
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -453,6 +522,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(method) &&
|
||||||
|
!preventCaching)
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,7 +593,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult<IRequest>(limitResult.Error!);
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -592,47 +668,47 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(T) == typeof(object))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
var error = new ServerError("Failed to parse response", accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Json response received
|
// Json response received
|
||||||
var parsedError = TryParseError(accessor);
|
var parsedError = TryParseError(accessor);
|
||||||
if (parsedError != null)
|
if (parsedError != null)
|
||||||
// Success status code, but TryParseError determined it was an error response
|
// Success status code, but TryParseError determined it was an error response
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
var deserializeResult = accessor.Deserialize<T>();
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException requestException)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
var exceptionInfo = requestException.ToLogString();
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -726,8 +802,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var headers = new Dictionary<string, string>();
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
if (AuthenticationProvider != null)
|
if (AuthenticationProvider != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -736,14 +812,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
this,
|
this,
|
||||||
uri,
|
uri,
|
||||||
method,
|
method,
|
||||||
parameters,
|
uriParameters,
|
||||||
|
bodyParameters,
|
||||||
|
headers,
|
||||||
signed,
|
signed,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
bodyFormat,
|
bodyFormat
|
||||||
out uriParameters,
|
);
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -804,12 +880,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="request">The request to set the parameters on</param>
|
/// <param name="request">The request to set the parameters on</param>
|
||||||
/// <param name="parameters">The parameters to set</param>
|
/// <param name="parameters">The parameters to set</param>
|
||||||
/// <param name="contentType">The content type of the data</param>
|
/// <param name="contentType">The content type of the data</param>
|
||||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
|
||||||
{
|
{
|
||||||
if (contentType == Constants.JsonContentHeader)
|
if (contentType == Constants.JsonContentHeader)
|
||||||
{
|
{
|
||||||
// Write the parameters as json in the body
|
// Write the parameters as json in the body
|
||||||
var stringData = CreateSerializer().Serialize(parameters);
|
string stringData;
|
||||||
|
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
|
||||||
|
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
|
||||||
|
else
|
||||||
|
stringData = CreateSerializer().Serialize(parameters);
|
||||||
request.SetContent(stringData, contentType);
|
request.SetContent(stringData, contentType);
|
||||||
}
|
}
|
||||||
else if (contentType == Constants.FormContentHeader)
|
else if (contentType == Constants.FormContentHeader)
|
||||||
@@ -859,6 +939,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new ServerRateLimitError(message);
|
return new ServerRateLimitError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the parameter IDictionary
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
|
||||||
|
{
|
||||||
|
if (!OrderParameters)
|
||||||
|
return parameters;
|
||||||
|
|
||||||
|
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -869,14 +962,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
if (timeSyncParams == null)
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -905,7 +998,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& definition.Method == HttpMethod.Get
|
||||||
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
|
private bool ShouldCache(HttpMethod method)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& method == HttpMethod.Get;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -52,11 +53,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true a subscription will accept message before the confirmation of a subscription has been received
|
|
||||||
/// </summary>
|
|
||||||
protected bool HandleMessageBeforeConfirmation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiters
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -72,6 +68,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of address to keep an alive connection to
|
||||||
|
/// </summary>
|
||||||
|
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -136,6 +137,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep an open connection to this url
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="auth"></param>
|
||||||
|
protected virtual void SetDedicatedConnection(string url, bool auth)
|
||||||
|
{
|
||||||
|
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -198,12 +209,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
|
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
// Add a subscription on the socket connection
|
||||||
var success = socketConnection.AddSubscription(subscription);
|
var success = socketConnection.AddSubscription(subscription);
|
||||||
@@ -250,11 +260,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
if (isTimeout && subscription.Confirmed)
|
||||||
var unsubscribe = subResult.Error is CancellationRequestedError;
|
{
|
||||||
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
// No response received, but the subscription did receive updates. We'll assume success
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||||
|
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||||
|
await socketConnection.CloseAsync(subscription, isTimeout).ConfigureAwait(false);
|
||||||
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
@@ -278,34 +295,41 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query)
|
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return QueryAsync(BaseAddress, query);
|
return QueryAsync(BaseAddress, query, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query on a socket connection and wait for the response
|
/// Send a query on a socket connection and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query)
|
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<T>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
@@ -318,7 +342,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<T>(connectResult.Error!);
|
return new CallResult<THandlerResponse>(connectResult.Error!);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -329,10 +353,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false);
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
|
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -356,7 +383,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
|
|
||||||
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -370,7 +401,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult(new NoApiCredentialsError());
|
return new CallResult(new NoApiCredentialsError());
|
||||||
|
|
||||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||||
var authRequest = GetAuthenticationRequest();
|
var authRequest = GetAuthenticationRequest(socket);
|
||||||
if (authRequest != null)
|
if (authRequest != null)
|
||||||
{
|
{
|
||||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||||
@@ -395,7 +426,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Should return the request which can be used to authenticate a socket connection
|
/// Should return the request which can be used to authenticate a socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
|
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
/// Adds a system subscription. Used for example to reply to ping requests
|
||||||
@@ -444,19 +475,31 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
|
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
&& s.Value.Connected);
|
||||||
if (result != null)
|
|
||||||
|
SocketConnection connection;
|
||||||
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(result);
|
return new CallResult<SocketConnection>(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
@@ -473,6 +516,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||||
|
|
||||||
foreach (var ptg in PeriodicTaskRegistrations)
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||||
@@ -515,7 +559,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="address">The address to connect to</param>
|
/// <param name="address">The address to connect to</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
@@ -592,8 +636,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
foreach (var sub in socketList)
|
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||||
tasks.Add(sub.CloseAsync());
|
tasks.Add(connection.CloseAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
@@ -616,6 +660,23 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<CallResult> PrepareConnectionsAsync()
|
||||||
|
{
|
||||||
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
|
{
|
||||||
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult(connectResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -699,11 +760,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
_logger.DisposingSocketClient();
|
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||||
_ = UnsubscribeAllAsync();
|
if (socketList.Any())
|
||||||
|
_logger.DisposingSocketClient();
|
||||||
|
|
||||||
|
foreach (var connection in socketList)
|
||||||
|
{
|
||||||
|
tasks.Add(connection.CloseAsync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -38,14 +39,8 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
var longValue = (long)reader.Value;
|
var longValue = (long)reader.Value;
|
||||||
if (longValue == 0 || longValue == -1)
|
if (longValue == 0 || longValue == -1)
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
return ParseFromLong(longValue);
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonToken.Float)
|
else if (reader.TokenType is JsonToken.Float)
|
||||||
{
|
{
|
||||||
@@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
return ParseFromString(stringValue);
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if(!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
}
|
||||||
else if(reader.TokenType == JsonToken.Date)
|
else if(reader.TokenType == JsonToken.Date)
|
||||||
{
|
{
|
||||||
@@ -150,6 +76,102 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromLong(long longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromString(string stringValue)
|
||||||
|
{
|
||||||
|
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
if (doubleValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
|
if (doubleValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 10)
|
||||||
|
{
|
||||||
|
// Parse 2021-11-03 format
|
||||||
|
var values = stringValue.Split('-');
|
||||||
|
if (!int.TryParse(values[0], out var year)
|
||||||
|
|| !int.TryParse(values[1], out var month)
|
||||||
|
|| !int.TryParse(values[2], out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> Read(Stream stream, bool bufferStream)
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
if (bufferStream && stream is not MemoryStream)
|
||||||
{
|
{
|
||||||
@@ -252,14 +252,15 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
{
|
{
|
||||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetOriginalString()
|
public override string GetOriginalString()
|
||||||
@@ -290,7 +291,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Read(ReadOnlyMemory<byte> data)
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
_bytes = data;
|
_bytes = data;
|
||||||
|
|
||||||
@@ -305,14 +306,14 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
{
|
{
|
||||||
_token = JToken.Load(jsonTextReader);
|
_token = JToken.Load(jsonTextReader);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
throw new Exception("1");
|
throw new Exception("Not an array");
|
||||||
|
|
||||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
||||||
attributes = CacheTypeAttributes(objectType);
|
attributes = CacheTypeAttributes(objectType);
|
||||||
@@ -92,8 +92,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||||
var targetType = attribute.PropertyInfo.PropertyType;
|
if (attribute == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var targetType = attribute.PropertyInfo.PropertyType;
|
||||||
|
|
||||||
object? value = null;
|
object? value = null;
|
||||||
if (attribute.JsonConverterType != null)
|
if (attribute.JsonConverterType != null)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -49,14 +50,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var longValue = reader.GetDouble();
|
var longValue = reader.GetDouble();
|
||||||
if (longValue == 0 || longValue == -1)
|
if (longValue == 0 || longValue == -1)
|
||||||
return default;
|
return default;
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
return ParseFromDouble(longValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
{
|
{
|
||||||
@@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
return ParseFromString(stringValue!);
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if (!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -160,6 +86,104 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromDouble(double longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromString(string stringValue)
|
||||||
|
{
|
||||||
|
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue <= 0)
|
||||||
|
return default;
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
if (doubleValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
|
if (doubleValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 10)
|
||||||
|
{
|
||||||
|
// Parse 2021-11-03 format
|
||||||
|
var values = stringValue.Split('-');
|
||||||
|
if (!int.TryParse(values[0], out var year)
|
||||||
|
|| !int.TryParse(values[1], out var month)
|
||||||
|
|| !int.TryParse(values[2], out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
writer.WriteStringValue("");
|
||||||
|
|
||||||
|
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var info = $"Unknown exception: {ex.Message}";
|
||||||
|
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -188,7 +193,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> Read(Stream stream, bool bufferStream)
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
if (bufferStream && stream is not MemoryStream)
|
||||||
{
|
{
|
||||||
@@ -211,15 +216,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetOriginalString()
|
public override string GetOriginalString()
|
||||||
{
|
{
|
||||||
@@ -249,7 +255,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Read(ReadOnlyMemory<byte> data)
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
_bytes = data;
|
_bytes = data;
|
||||||
|
|
||||||
@@ -257,14 +263,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_document = JsonDocument.Parse(data);
|
_document = JsonDocument.Parse(data);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -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.3.3</PackageVersion>
|
<PackageVersion>7.7.2</PackageVersion>
|
||||||
<AssemblyVersion>7.3.3</AssemblyVersion>
|
<AssemblyVersion>7.7.2</AssemblyVersion>
|
||||||
<FileVersion>7.3.3</FileVersion>
|
<FileVersion>7.7.2</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>
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ namespace CryptoExchange.Net
|
|||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var kvp in parameters)
|
foreach (var kvp in parameters)
|
||||||
{
|
{
|
||||||
|
if (kvp.Value is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
if (kvp.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
var array = (Array)kvp.Value;
|
||||||
@@ -342,7 +345,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="baseUri"></param>
|
/// <param name="baseUri"></param>
|
||||||
/// <param name="arraySerialization"></param>
|
/// <param name="arraySerialization"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||||
{
|
{
|
||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Time provider
|
||||||
|
/// </summary>
|
||||||
|
internal interface IAuthTimeProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get current time
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
DateTime GetTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,14 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
string BaseAddress { get; }
|
string BaseAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format a base and quote asset to an exchange accepted symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAsset">The base asset</param>
|
||||||
|
/// <param name="quoteAsset">The quote asset</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string FormatSymbol(string baseAsset, string quoteAsset);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this API client
|
/// Set the API credentials for this API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream"></param>
|
/// <param name="stream"></param>
|
||||||
/// <param name="bufferStream"></param>
|
/// <param name="bufferStream"></param>
|
||||||
Task<bool> Read(Stream stream, bool bufferStream);
|
Task<CallResult> Read(Stream stream, bool bufferStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,6 +96,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Load a data message
|
/// Load a data message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
bool Read(ReadOnlyMemory<byte> data);
|
CallResult Read(ReadOnlyMemory<byte> data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this listener can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The identifiers for this processor
|
/// The identifiers for this processor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<string> ListenerIdentifiers { get; }
|
public HashSet<string> ListenerIdentifiers { get; }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for ISymbolOrderBook instances
|
||||||
|
/// </summary>
|
||||||
|
public interface IOrderBookFactory<TOptions> where TOptions : OrderBookOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new order book by symbol name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol name</param>
|
||||||
|
/// <param name="options">Options for the order book</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null);
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new order book by base and quote asset names
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAsset">Base asset name</param>
|
||||||
|
/// <param name="quoteAsset">Quote asset name</param>
|
||||||
|
/// <param name="options">Options for the order book</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepare connections which can subsequently be used for sending websocket requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CallResult> PrepareConnectionsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
||||||
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||||
|
|
||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
@@ -65,6 +68,21 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4008, "RestApiSendRequest"),
|
new EventId(4008, "RestApiSendRequest"),
|
||||||
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||||
|
|
||||||
|
_restApiCheckingCache = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4009, "RestApiCheckingCache"),
|
||||||
|
"Checking cache for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4010, "RestApiCacheHit"),
|
||||||
|
"Cache hit for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4011, "RestApiCacheNotHit"),
|
||||||
|
"Cache not hit for key {Key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
||||||
@@ -111,5 +129,20 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void CheckingCache(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCheckingCache(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheHit(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheNotHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheNotHit(logger, key, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Wait for the AutoResetEvent to be set
|
/// Wait for the AutoResetEvent to be set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task<bool> WaitAsync(TimeSpan? timeout = null)
|
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock (_waits)
|
lock (_waits)
|
||||||
{
|
{
|
||||||
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
if (ct.IsCancellationRequested)
|
||||||
if(timeout != null)
|
return _completed;
|
||||||
{
|
|
||||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
|
||||||
var registration = cancellationSource.Token.Register(() =>
|
|
||||||
{
|
|
||||||
lock (_waits)
|
|
||||||
{
|
|
||||||
tcs.TrySetResult(false);
|
|
||||||
|
|
||||||
// Not the cleanest but it works
|
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
if (timeout.HasValue)
|
||||||
}
|
{
|
||||||
}, useSynchronizationContext: false);
|
var timeoutSource = new CancellationTokenSource(timeout.Value);
|
||||||
|
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
|
||||||
|
ct = cancellationSource.Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var registration = ct.Register(() =>
|
||||||
|
{
|
||||||
|
lock (_waits)
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(false);
|
||||||
|
|
||||||
|
// Not the cleanest but it works
|
||||||
|
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||||
|
}
|
||||||
|
}, useSynchronizationContext: false);
|
||||||
|
|
||||||
|
|
||||||
_waits.Enqueue(tcs);
|
_waits.Enqueue(tcs);
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
internal class AuthTimeProvider : IAuthTimeProvider
|
||||||
|
{
|
||||||
|
public DateTime GetTime() => DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -331,6 +331,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? ResponseTime { get; set; }
|
public TimeSpan? ResponseTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of this result
|
||||||
|
/// </summary>
|
||||||
|
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new result
|
/// Create a new result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -344,6 +349,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBody"></param>
|
/// <param name="requestBody"></param>
|
||||||
/// <param name="requestMethod"></param>
|
/// <param name="requestMethod"></param>
|
||||||
/// <param name="requestHeaders"></param>
|
/// <param name="requestHeaders"></param>
|
||||||
|
/// <param name="dataSource"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
@@ -357,6 +363,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||||
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
{
|
{
|
||||||
@@ -370,6 +377,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
RequestHeaders = requestHeaders;
|
RequestHeaders = requestHeaders;
|
||||||
RequestMethod = requestMethod;
|
RequestMethod = requestMethod;
|
||||||
|
DataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -393,7 +401,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <param name="error">The error</param>
|
||||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -403,7 +411,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -414,7 +422,16 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
public new WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a copy of this result with data source set to cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
internal WebCallResult<T> Cached()
|
||||||
|
{
|
||||||
|
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -13,5 +13,9 @@
|
|||||||
/// Form content type header
|
/// Form content type header
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FormContentHeader = "application/x-www-form-urlencoded";
|
public const string FormContentHeader = "application/x-www-form-urlencoded";
|
||||||
|
/// <summary>
|
||||||
|
/// Placeholder key for when request body should be set to the value of this KVP
|
||||||
|
/// </summary>
|
||||||
|
public const string BodyPlaceHolderKey = "_BODY_";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,4 +169,38 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Snapshot
|
Snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public enum ReconnectPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect is disabled
|
||||||
|
/// </summary>
|
||||||
|
Disabled,
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed delay of `ReconnectInterval` between retries
|
||||||
|
/// </summary>
|
||||||
|
FixedDelay,
|
||||||
|
/// <summary>
|
||||||
|
/// Backof policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5
|
||||||
|
/// </summary>
|
||||||
|
ExponentialBackoff
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of the result
|
||||||
|
/// </summary>
|
||||||
|
public enum ResultDataSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// From server
|
||||||
|
/// </summary>
|
||||||
|
Server,
|
||||||
|
/// <summary>
|
||||||
|
/// From cache
|
||||||
|
/// </summary>
|
||||||
|
Cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for order book options
|
/// Base for order book options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OrderBookOptions : ExchangeOptions
|
public class OrderBookOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||||
@@ -19,11 +19,7 @@
|
|||||||
{
|
{
|
||||||
return new T
|
return new T
|
||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
|
||||||
OutputOriginalData = OutputOriginalData,
|
|
||||||
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
||||||
Proxy = Proxy,
|
|
||||||
RequestTimeout = RequestTimeout
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option
|
||||||
|
/// </summary>
|
||||||
|
public bool CachingEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a copy of this options
|
/// Create a copy of this options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
Proxy = Proxy,
|
Proxy = Proxy,
|
||||||
RequestTimeout = RequestTimeout,
|
RequestTimeout = RequestTimeout,
|
||||||
RateLimiterEnabled = RateLimiterEnabled,
|
RateLimiterEnabled = RateLimiterEnabled,
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour
|
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||||
|
CachingEnabled = CachingEnabled,
|
||||||
|
CachingMaxAge = CachingMaxAge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
@@ -9,15 +10,15 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
public class SocketExchangeOptions : ExchangeOptions
|
public class SocketExchangeOptions : ExchangeOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not the socket should automatically reconnect when losing connection
|
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time to wait between reconnect attempts
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
AutoReconnect = AutoReconnect,
|
ReconnectPolicy = ReconnectPolicy,
|
||||||
DelayAfterConnect = DelayAfterConnect,
|
DelayAfterConnect = DelayAfterConnect,
|
||||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||||
ReconnectInterval = ReconnectInterval,
|
ReconnectInterval = ReconnectInterval,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order string comparer, sorts by alphabetical order
|
||||||
|
/// </summary>
|
||||||
|
public class OrderedStringComparer : IComparer<string>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Compare function
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x"></param>
|
||||||
|
/// <param name="y"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int Compare(string x, string y)
|
||||||
|
{
|
||||||
|
// Shortcuts: If both are null, they are the same.
|
||||||
|
if (x == null && y == null) return 0;
|
||||||
|
|
||||||
|
// If one is null and the other isn't, then the
|
||||||
|
// one that is null is "lesser".
|
||||||
|
if (x == null) return -1;
|
||||||
|
if (y == null) return 1;
|
||||||
|
|
||||||
|
return x.CompareTo(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.SystemTextJson;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects
|
namespace CryptoExchange.Net.Objects
|
||||||
{
|
{
|
||||||
@@ -193,5 +194,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Add(key, int.Parse(stringVal));
|
Add(key, int.Parse(stringVal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="body">Body to set</param>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public void SetBody(object body)
|
||||||
|
{
|
||||||
|
if (this.Any())
|
||||||
|
throw new InvalidOperationException("Can't set body when other parameters already specified");
|
||||||
|
|
||||||
|
Add(Constants.BodyPlaceHolderKey, body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
public TimeSpan? EndpointLimitPeriod { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this request should never be cached
|
||||||
|
/// </summary>
|
||||||
|
public bool PreventCaching { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBodyFormat">Request body format</param>
|
/// <param name="requestBodyFormat">Request body format</param>
|
||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@@ -59,7 +60,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
TimeSpan? endpointLimitPeriod = null,
|
TimeSpan? endpointLimitPeriod = null,
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null)
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
bool? preventCaching = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(method + path, out var def))
|
if (!_definitions.TryGetValue(method + path, out var def))
|
||||||
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
|
PreventCaching = preventCaching ?? false
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(method + path, def);
|
_definitions.TryAdd(method + path, def);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The topic of the update, what symbol/asset etc..
|
/// The stream producing the update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Topic { get; set; }
|
public string? StreamId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The symbol the update is for
|
||||||
|
/// </summary>
|
||||||
|
public string? Symbol { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
||||||
@@ -33,10 +38,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
|
|
||||||
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
internal DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Topic = topic;
|
StreamId = streamId;
|
||||||
|
Symbol = symbol;
|
||||||
OriginalData = originalData;
|
OriginalData = originalData;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
UpdateType = updateType;
|
UpdateType = updateType;
|
||||||
@@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data)
|
public DataEvent<K> As<K>(K data)
|
||||||
{
|
{
|
||||||
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType);
|
return new DataEvent<K>(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="K">The type of the new data</typeparam>
|
/// <typeparam name="K">The type of the new data</typeparam>
|
||||||
/// <param name="data">The new data</param>
|
/// <param name="data">The new data</param>
|
||||||
/// <param name="topic">The new topic</param>
|
/// <param name="symbol">The new symbol</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data, string? topic)
|
public DataEvent<K> As<K>(K data, string? symbol)
|
||||||
{
|
{
|
||||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType);
|
return new DataEvent<K>(data, StreamId, symbol, OriginalData, Timestamp, UpdateType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,12 +76,73 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="K">The type of the new data</typeparam>
|
/// <typeparam name="K">The type of the new data</typeparam>
|
||||||
/// <param name="data">The new data</param>
|
/// <param name="data">The new data</param>
|
||||||
/// <param name="topic">The new topic</param>
|
/// <param name="streamId">The new stream id</param>
|
||||||
|
/// <param name="symbol">The new symbol</param>
|
||||||
/// <param name="updateType">The type of update</param>
|
/// <param name="updateType">The type of update</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType)
|
public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
|
||||||
{
|
{
|
||||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType);
|
return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataEvent<T> WithSymbol(string symbol)
|
||||||
|
{
|
||||||
|
Symbol = symbol;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the update type
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataEvent<T> WithUpdateType(SocketUpdateType type)
|
||||||
|
{
|
||||||
|
UpdateType = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the stream id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="streamId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataEvent<T> WithStreamId(string streamId)
|
||||||
|
{
|
||||||
|
StreamId = streamId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a CallResult from this DataEvent
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<T> ToCallResult()
|
||||||
|
{
|
||||||
|
return new CallResult<T>(Data, OriginalData, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a CallResult from this DataEvent
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<K> ToCallResult<K>(K data)
|
||||||
|
{
|
||||||
|
return new CallResult<K>(data, OriginalData, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a CallResult from this DataEvent
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<K> ToCallResult<K>(Error error)
|
||||||
|
{
|
||||||
|
return new CallResult<K>(default, OriginalData, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,20 +26,20 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time to wait between reconnect attempts
|
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy for the connection
|
/// Proxy for the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiProxy? Proxy { get; set; }
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the socket should automatically reconnect when connection is lost
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">Uri</param>
|
/// <param name="uri">Uri</param>
|
||||||
/// <param name="autoReconnect">Auto reconnect</param>
|
/// <param name="policy">Reconnect policy</param>
|
||||||
public WebSocketParameters(Uri uri, bool autoReconnect)
|
public WebSocketParameters(Uri uri, ReconnectPolicy policy)
|
||||||
{
|
{
|
||||||
Uri = uri;
|
Uri = uri;
|
||||||
AutoReconnect = autoReconnect;
|
ReconnectPolicy = policy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.OrderBook
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions
|
||||||
|
{
|
||||||
|
private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor;
|
||||||
|
private readonly Func<string, string, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbolCtor"></param>
|
||||||
|
/// <param name="assetsCtor"></param>
|
||||||
|
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<string, string, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
|
||||||
|
{
|
||||||
|
_symbolCtor = symbolCtor;
|
||||||
|
_assetsCtor = assetsCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(baseAsset, quoteAsset, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||||
|
|
||||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||||
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
|
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||||
{
|
{
|
||||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||||
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime()
|
private TimeSpan DetermineWaitTime()
|
||||||
{
|
{
|
||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||||
return wait.Add(_fixedWindowBuffer);
|
var result = wait.Add(_fixedWindowBuffer);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private readonly List<LimitEntry> _entries;
|
private readonly List<LimitEntry> _entries;
|
||||||
private int _currentWeight = 0;
|
private int _currentWeight = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional wait time to apply to account for fluctuating request times
|
||||||
|
/// </summary>
|
||||||
|
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||||
{
|
{
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
removedWeight += entry.Weight;
|
removedWeight += entry.Weight;
|
||||||
if (removedWeight >= weightToRemove)
|
if (removedWeight >= weightToRemove)
|
||||||
{
|
{
|
||||||
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
|
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Requests
|
|||||||
if (client == null)
|
if (client == null)
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler();
|
var handler = new HttpClientHandler();
|
||||||
|
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
if (proxy != null)
|
if (proxy != null)
|
||||||
{
|
{
|
||||||
handler.Proxy = new WebProxy
|
handler.Proxy = new WebProxy
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
private ProcessState _processState;
|
private ProcessState _processState;
|
||||||
private DateTime _lastReconnectTime;
|
private DateTime _lastReconnectTime;
|
||||||
private string _baseAddress;
|
private string _baseAddress;
|
||||||
|
private int _reconnectAttempt;
|
||||||
|
|
||||||
private const int _receiveBufferSize = 1048576;
|
private const int _receiveBufferSize = 1048576;
|
||||||
private const int _sendBufferSize = 4096;
|
private const int _sendBufferSize = 4096;
|
||||||
@@ -246,12 +247,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await _closeTask.ConfigureAwait(false);
|
await _closeTask.ConfigureAwait(false);
|
||||||
_closeTask = null;
|
_closeTask = null;
|
||||||
|
|
||||||
if (!Parameters.AutoReconnect)
|
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
|
||||||
{
|
{
|
||||||
_processState = ProcessState.Idle;
|
_processState = ProcessState.Idle;
|
||||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_stopRequested)
|
if (!_stopRequested)
|
||||||
{
|
{
|
||||||
@@ -259,9 +260,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
// Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected
|
||||||
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
var initialDelay = GetReconnectDelay();
|
||||||
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
await Task.Delay(initialDelay).ConfigureAwait(false);
|
||||||
|
|
||||||
while (!_stopRequested)
|
while (!_stopRequested)
|
||||||
{
|
{
|
||||||
@@ -282,13 +283,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||||
|
|
||||||
|
_reconnectAttempt++;
|
||||||
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||||
if (!connected)
|
if (!connected)
|
||||||
{
|
{
|
||||||
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
// Delay between reconnect attempts
|
||||||
|
var delay = GetReconnectDelay();
|
||||||
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_reconnectAttempt = 0;
|
||||||
_lastReconnectTime = DateTime.UtcNow;
|
_lastReconnectTime = DateTime.UtcNow;
|
||||||
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
@@ -298,6 +303,24 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_processState = ProcessState.Idle;
|
_processState = ProcessState.Idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TimeSpan GetReconnectDelay()
|
||||||
|
{
|
||||||
|
if (_reconnectAttempt == 0)
|
||||||
|
{
|
||||||
|
// Means this is directly after disconnecting. Only delay if the last reconnect time is very recent
|
||||||
|
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||||
|
if (sinceLastReconnect < TimeSpan.FromSeconds(5))
|
||||||
|
return TimeSpan.FromSeconds(5) - sinceLastReconnect;
|
||||||
|
|
||||||
|
return TimeSpan.FromMilliseconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var delay = Parameters.ReconnectPolicy == ReconnectPolicy.FixedDelay ? Parameters.ReconnectInterval : TimeSpan.FromSeconds(Math.Pow(2, Math.Min(5, _reconnectAttempt)));
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
return delay;
|
||||||
|
return TimeSpan.FromMilliseconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(int id, string data, int weight)
|
public virtual void Send(int id, string data, int weight)
|
||||||
{
|
{
|
||||||
@@ -439,11 +462,19 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
if (Parameters.RateLimiter != null)
|
if (Parameters.RateLimiter != null)
|
||||||
{
|
{
|
||||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
try
|
||||||
if (!limitResult)
|
|
||||||
{
|
{
|
||||||
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
||||||
continue;
|
if (!limitResult)
|
||||||
|
{
|
||||||
|
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// canceled
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,7 +760,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The request id
|
/// The request weight
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Weight { get; set; }
|
public int Weight { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dedicated connection configuration
|
||||||
|
/// </summary>
|
||||||
|
public class DedicatedConnectionConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket address
|
||||||
|
/// </summary>
|
||||||
|
public string SocketAddress { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// authenticated
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Requests;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -18,11 +19,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; } = ExchangeHelpers.NextId();
|
public int Id { get; } = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData => true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Has this query been completed
|
/// Has this query been completed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -115,8 +111,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Wait untill timeout or the request is competed
|
/// Wait untill timeout or the request is competed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="timeout"></param>
|
/// <param name="timeout"></param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false);
|
public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
||||||
@@ -145,16 +142,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query
|
/// Query
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TResponse">Response object type</typeparam>
|
/// <typeparam name="TServerResponse">The type returned from the server</typeparam>
|
||||||
public abstract class Query<TResponse> : Query
|
/// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
|
||||||
|
public abstract class Query<TServerResponse, THandlerResponse> : Query
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse);
|
public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed call result
|
/// The typed call result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result;
|
public CallResult<THandlerResponse>? TypedResult => (CallResult<THandlerResponse>?)Result;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -171,7 +169,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
Completed = true;
|
Completed = true;
|
||||||
Response = message.Data;
|
Response = message.Data;
|
||||||
Result = HandleMessage(connection, message.As((TResponse)message.Data));
|
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
|
||||||
_event.Set();
|
_event.Set();
|
||||||
ContinueAwaiter?.WaitOne();
|
ContinueAwaiter?.WaitOne();
|
||||||
return Result;
|
return Result;
|
||||||
@@ -183,7 +181,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => new CallResult<TResponse>(message.Data, message.OriginalData, null);
|
public abstract CallResult<THandlerResponse> HandleMessage(SocketConnection connection, DataEvent<TServerResponse> message);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Timeout()
|
public override void Timeout()
|
||||||
@@ -192,7 +190,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Completed = true;
|
Completed = true;
|
||||||
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null));
|
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
|
||||||
ContinueAwaiter?.Set();
|
ContinueAwaiter?.Set();
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
@@ -200,10 +198,35 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Fail(Error error)
|
public override void Fail(Error error)
|
||||||
{
|
{
|
||||||
Result = new CallResult<TResponse>(error);
|
Result = new CallResult<THandlerResponse>(error);
|
||||||
Completed = true;
|
Completed = true;
|
||||||
ContinueAwaiter?.Set();
|
ContinueAwaiter?.Set();
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Response object type</typeparam>
|
||||||
|
public abstract class Query<TResponse> : Query<TResponse, TResponse>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
|
/// <param name="weight"></param>
|
||||||
|
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle the query response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => message.ToCallResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this connection should be kept alive even when there is no subscription
|
||||||
|
/// </summary>
|
||||||
|
public bool DedicatedRequestConnection { get; internal set; }
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
private readonly object _listenersLock;
|
private readonly object _listenersLock;
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
@@ -443,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// 4. Get the listeners interested in this message
|
// 4. Get the listeners interested in this message
|
||||||
List<IMessageProcessor> processors;
|
List<IMessageProcessor> processors;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList();
|
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
|
||||||
|
|
||||||
if (processors.Count == 0)
|
if (processors.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -451,7 +456,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
List<string> listenerIds;
|
List<string> listenerIds;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
listenerIds = _listeners.Where(l => l.CanHandleData).SelectMany(l => l.ListenerIdentifiers).ToList();
|
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
||||||
UnhandledMessage?.Invoke(_accessor);
|
UnhandledMessage?.Invoke(_accessor);
|
||||||
}
|
}
|
||||||
@@ -478,6 +483,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
|
||||||
|
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||||
|
subscriptionProcessor.Confirmed = true;
|
||||||
|
|
||||||
// 6. Deserialize the message
|
// 6. Deserialize the message
|
||||||
object? deserialized = null;
|
object? deserialized = null;
|
||||||
desCache?.TryGetValue(messageType, out deserialized);
|
desCache?.TryGetValue(messageType, out deserialized);
|
||||||
@@ -498,7 +507,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var innerSw = Stopwatch.StartNew();
|
var innerSw = Stopwatch.StartNew();
|
||||||
processor.Handle(this, new DataEvent<object>(deserialized, null, originalData, receiveTime, null));
|
processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null));
|
||||||
totalUserTime += (int)innerSw.ElapsedMilliseconds;
|
totalUserTime += (int)innerSw.ElapsedMilliseconds;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -604,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
{
|
{
|
||||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed);
|
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||||
if (shouldCloseConnection)
|
if (shouldCloseConnection)
|
||||||
Status = SocketStatus.Closing;
|
Status = SocketStatus.Closing;
|
||||||
}
|
}
|
||||||
@@ -686,27 +695,30 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">Query to send</param>
|
/// <param name="query">Query to send</param>
|
||||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null)
|
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
|
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||||
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query request and wait for an answer
|
/// Send a query request and wait for an answer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Query response type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="query">Query to send</param>
|
/// <param name="query">Query to send</param>
|
||||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult<T>> SendAndWaitQueryAsync<T>(Query<T> query, ManualResetEvent? continueEvent = null)
|
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, ManualResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
|
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||||
return query.TypedResult ?? new CallResult<T>(new ServerError("Timeout"));
|
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent)
|
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock(_listenersLock)
|
lock(_listenersLock)
|
||||||
_listeners.Add(query);
|
_listeners.Add(query);
|
||||||
@@ -723,7 +735,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
{
|
{
|
||||||
@@ -734,11 +746,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (query.Completed)
|
if (query.Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await query.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
|
await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
if (query.Completed)
|
if (query.Completed)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
query.Fail(new CancellationRequestedError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -798,20 +816,23 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
bool anySubscriptions;
|
if (!DedicatedRequestConnection)
|
||||||
lock (_listenersLock)
|
|
||||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
|
||||||
if (!anySubscriptions)
|
|
||||||
{
|
{
|
||||||
// No need to resubscribe anything
|
bool anySubscriptions;
|
||||||
_logger.NothingToResubscribeCloseConnection(SocketId);
|
lock (_listenersLock)
|
||||||
_ = _socket.CloseAsync();
|
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||||
return new CallResult(null);
|
if (!anySubscriptions)
|
||||||
|
{
|
||||||
|
// No need to resubscribe anything
|
||||||
|
_logger.NothingToResubscribeCloseConnection(SocketId);
|
||||||
|
_ = _socket.CloseAsync();
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool anyAuthenticated;
|
bool anyAuthenticated;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated);
|
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated) || DedicatedRequestConnection;
|
||||||
if (anyAuthenticated)
|
if (anyAuthenticated)
|
||||||
{
|
{
|
||||||
// If we reconnected a authenticated connection we need to re-authenticate
|
// If we reconnected a authenticated connection we need to re-authenticate
|
||||||
@@ -860,6 +881,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
waitEvent.Set();
|
waitEvent.Set();
|
||||||
|
if (r.Result.Success)
|
||||||
|
subscription.Confirmed = true;
|
||||||
return r.Result;
|
return r.Result;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -869,9 +892,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return taskList.First(t => !t.Result.Success).Result;
|
return taskList.First(t => !t.Result.Success).Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subscription in subList)
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
|
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total amount of invocations
|
/// Total amount of invocations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,11 +37,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Has the subscription been confirmed
|
/// Has the subscription been confirmed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Confirmed { get; set; }
|
public bool Confirmed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this subscription should handle update messages before confirmation
|
|
||||||
/// </summary>
|
|
||||||
public bool HandleUpdatesBeforeConfirmation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the subscription closed
|
/// Is the subscription closed
|
||||||
|
|||||||
@@ -0,0 +1,379 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Comparers
|
||||||
|
{
|
||||||
|
internal class JsonNetComparer
|
||||||
|
{
|
||||||
|
internal static void CompareData(
|
||||||
|
string method,
|
||||||
|
object resultData,
|
||||||
|
string json,
|
||||||
|
string? nestedJsonProperty,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool userSingleArrayItem = false)
|
||||||
|
{
|
||||||
|
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault()));
|
||||||
|
var jsonObject = JToken.Parse(json);
|
||||||
|
if (nestedJsonProperty != null)
|
||||||
|
{
|
||||||
|
var nested = nestedJsonProperty.Split('.');
|
||||||
|
foreach (var nest in nested)
|
||||||
|
jsonObject = jsonObject![nest];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSingleArrayItem)
|
||||||
|
jsonObject = ((JArray)jsonObject!)[0];
|
||||||
|
|
||||||
|
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)resultData;
|
||||||
|
var jObj = (JObject)jsonObject!;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
// TODO Some additional checking for objects
|
||||||
|
foreach (var prop in ((JObject)dictProp.Value).Properties())
|
||||||
|
CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)jsonObject;
|
||||||
|
if (resultData is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in jsonObject)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, resultData, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Successfully validated {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||||
|
{
|
||||||
|
var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault())?.PropertyName));
|
||||||
|
|
||||||
|
// Property has a value
|
||||||
|
var property = resultProperties.SingleOrDefault(p => p.PropertyName == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
// Property not found
|
||||||
|
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||||
|
|
||||||
|
var propertyValue = property.GetValue(obj);
|
||||||
|
if (property.GetCustomAttribute<JsonPropertyAttribute>(true)?.ItemConverterType == null)
|
||||||
|
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||||
|
{
|
||||||
|
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
if (propValue.ToString() != "0")
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)propertyValue;
|
||||||
|
var jObj = (JObject)propValue;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||||
|
&& propertyValue.GetType() != typeof(string))
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
var list = (IEnumerable)propertyValue;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (JToken jtoken in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||||
|
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
|
||||||
|
// Custom converter for the type, skip
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (jtoken.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jtoken).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jtoken.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jtoken.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jtoken).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}");
|
||||||
|
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (propValue.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var item in propValue)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, propertyValue, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(propValue.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
if (propertyValue is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue)
|
||||||
|
{
|
||||||
|
if (jsonValue.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
if (objectValue is decimal dec)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<decimal>() != dec)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {dec}");
|
||||||
|
}
|
||||||
|
else if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (!jsonValue.Value<string>()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromLong(jsonValue.Value<long>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
|
||||||
|
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Comparers
|
||||||
|
{
|
||||||
|
internal class SystemTextJsonComparer
|
||||||
|
{
|
||||||
|
internal static void CompareData(
|
||||||
|
string method,
|
||||||
|
object resultData,
|
||||||
|
string json,
|
||||||
|
string? nestedJsonProperty,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool userSingleArrayItem = false)
|
||||||
|
{
|
||||||
|
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault()));
|
||||||
|
var jsonObject = JToken.Parse(json);
|
||||||
|
if (nestedJsonProperty != null)
|
||||||
|
{
|
||||||
|
var nested = nestedJsonProperty.Split('.');
|
||||||
|
foreach(var nest in nested)
|
||||||
|
jsonObject = jsonObject![nest];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSingleArrayItem)
|
||||||
|
jsonObject = ((JArray)jsonObject!)[0];
|
||||||
|
|
||||||
|
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)resultData;
|
||||||
|
var jObj = (JObject)jsonObject!;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
// TODO Some additional checking for objects
|
||||||
|
foreach (var prop in ((JObject)dictProp.Value).Properties())
|
||||||
|
CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
{
|
||||||
|
if (dictProp.Value.ToString() == "")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)jsonObject;
|
||||||
|
var list = (IEnumerable)resultData;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in jsonObject)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, resultData, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Successfully validated {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||||
|
{
|
||||||
|
var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||||
|
|
||||||
|
// Property has a value
|
||||||
|
var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
// Property not found
|
||||||
|
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||||
|
|
||||||
|
var propertyValue = property.GetValue(obj);
|
||||||
|
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||||
|
{
|
||||||
|
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
if (propValue.ToString() != "0")
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)propertyValue;
|
||||||
|
var jObj = (JObject)propValue;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||||
|
&& propertyValue.GetType() != typeof(string))
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
var list = (IEnumerable)propertyValue;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (JToken jtoken in jObjs)
|
||||||
|
{
|
||||||
|
var moved = enumerator.MoveNext();
|
||||||
|
if (!moved)
|
||||||
|
throw new Exception("Enumeration not moved; incorrect amount of results?");
|
||||||
|
|
||||||
|
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||||
|
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
|
||||||
|
// Custom converter for the type, skip
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (jtoken.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jtoken).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jtoken.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jtoken.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jtoken).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}");
|
||||||
|
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (propValue.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var item in propValue)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, propertyValue, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue)
|
||||||
|
{
|
||||||
|
if (jsonValue.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
if (objectValue is decimal dec)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<decimal>() != dec)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {dec}");
|
||||||
|
}
|
||||||
|
else if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (!jsonValue.Value<string>()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<bool>() != (bool)objectValue)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
internal class EnumValueTraceListener : TraceListener
|
||||||
|
{
|
||||||
|
public override void Write(string message)
|
||||||
|
{
|
||||||
|
if (message.Contains("Cannot map"))
|
||||||
|
throw new Exception("Enum value error: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteLine(string message)
|
||||||
|
{
|
||||||
|
if (message.Contains("Cannot map"))
|
||||||
|
throw new Exception("Enum value error: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestAuthTimeProvider : IAuthTimeProvider
|
||||||
|
{
|
||||||
|
private readonly DateTime _timestamp;
|
||||||
|
|
||||||
|
public TestAuthTimeProvider(DateTime timestamp)
|
||||||
|
{
|
||||||
|
_timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime GetTime() => _timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test implementation for nonce provider, returning a prespecified nonce
|
||||||
|
/// </summary>
|
||||||
|
public class TestNonceProvider : INonceProvider
|
||||||
|
{
|
||||||
|
private readonly long _nonce;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public TestNonceProvider(long nonce)
|
||||||
|
{
|
||||||
|
_nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long GetNonce() => _nonce;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRequest : IRequest
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IEnumerable<string>> _headers = new Dictionary<string, IEnumerable<string>>();
|
||||||
|
private readonly TestResponse _response;
|
||||||
|
|
||||||
|
public string Accept { set { } }
|
||||||
|
|
||||||
|
public string? Content { get; private set; }
|
||||||
|
|
||||||
|
public HttpMethod Method { get; set; }
|
||||||
|
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public TestRequest(TestResponse response)
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
{
|
||||||
|
_response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddHeader(string key, string value)
|
||||||
|
{
|
||||||
|
_headers.Add(key, new[] { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, IEnumerable<string>> GetHeaders() => _headers;
|
||||||
|
|
||||||
|
public Task<IResponse> GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult<IResponse>(_response);
|
||||||
|
|
||||||
|
public void SetContent(byte[] data)
|
||||||
|
{
|
||||||
|
Content = Encoding.UTF8.GetString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetContent(string data, string contentType)
|
||||||
|
{
|
||||||
|
Content = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRequestFactory : IRequestFactory
|
||||||
|
{
|
||||||
|
private readonly TestRequest _request;
|
||||||
|
|
||||||
|
public TestRequestFactory(TestRequest request)
|
||||||
|
{
|
||||||
|
_request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||||
|
{
|
||||||
|
_request.Method = method;
|
||||||
|
_request.Uri = uri;
|
||||||
|
_request.RequestId = requestId;
|
||||||
|
return _request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestResponse : IResponse
|
||||||
|
{
|
||||||
|
private readonly Stream _response;
|
||||||
|
|
||||||
|
public HttpStatusCode StatusCode { get; }
|
||||||
|
|
||||||
|
public bool IsSuccessStatusCode { get; }
|
||||||
|
|
||||||
|
public long? ContentLength { get; }
|
||||||
|
|
||||||
|
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; } = new Dictionary<string, IEnumerable<string>>();
|
||||||
|
|
||||||
|
public TestResponse(HttpStatusCode code, Stream response)
|
||||||
|
{
|
||||||
|
StatusCode = code;
|
||||||
|
IsSuccessStatusCode = code == HttpStatusCode.OK;
|
||||||
|
_response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream> GetResponseStreamAsync() => Task.FromResult(_response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocket : IWebsocket
|
||||||
|
{
|
||||||
|
public event Action<string>? OnMessageSend;
|
||||||
|
|
||||||
|
public bool CanConnect { get; set; } = true;
|
||||||
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
|
public event Func<Task>? OnClose;
|
||||||
|
#pragma warning disable 0067
|
||||||
|
public event Func<Task>? OnReconnected;
|
||||||
|
public event Func<Task>? OnReconnecting;
|
||||||
|
public event Func<int, Task>? OnRequestRateLimited;
|
||||||
|
public event Func<Exception, Task>? OnError;
|
||||||
|
#pragma warning restore 0067
|
||||||
|
public event Func<int, Task>? OnRequestSent;
|
||||||
|
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>>? OnStreamMessage;
|
||||||
|
public event Func<Task>? OnOpen;
|
||||||
|
|
||||||
|
public int Id { get; }
|
||||||
|
public bool IsClosed => !Connected;
|
||||||
|
public bool IsOpen => Connected;
|
||||||
|
public double IncomingKbps => 0;
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
|
public TestSocket(string address)
|
||||||
|
{
|
||||||
|
Uri = new Uri(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CallResult> ConnectAsync()
|
||||||
|
{
|
||||||
|
Connected = CanConnect;
|
||||||
|
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(int requestId, string data, int weight)
|
||||||
|
{
|
||||||
|
if (!Connected)
|
||||||
|
throw new Exception("Socket not connected");
|
||||||
|
|
||||||
|
OnRequestSent?.Invoke(requestId);
|
||||||
|
OnMessageSend?.Invoke(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CloseAsync()
|
||||||
|
{
|
||||||
|
Connected = false;
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeClose()
|
||||||
|
{
|
||||||
|
Connected = false;
|
||||||
|
OnClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeOpen()
|
||||||
|
{
|
||||||
|
OnOpen?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeMessage(string data)
|
||||||
|
{
|
||||||
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeMessage<T>(T data)
|
||||||
|
{
|
||||||
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReconnectAsync() => throw new NotImplementedException();
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestWebsocketFactory : IWebsocketFactory
|
||||||
|
{
|
||||||
|
private readonly TestSocket _socket;
|
||||||
|
public TestWebsocketFactory(TestSocket socket)
|
||||||
|
{
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) => _socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Testing.Comparers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for REST requests, comparing path, http method, authentication and response parsing
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient">The Rest client</typeparam>
|
||||||
|
public class RestRequestValidator<TClient> where TClient : BaseRestClient
|
||||||
|
{
|
||||||
|
private readonly TClient _client;
|
||||||
|
private readonly Func<WebCallResult, bool> _isAuthenticated;
|
||||||
|
private readonly string _folder;
|
||||||
|
private readonly string _baseAddress;
|
||||||
|
private readonly string? _nestedPropertyForCompare;
|
||||||
|
private readonly bool _stjCompare;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client to test</param>
|
||||||
|
/// <param name="folder">Folder for json test values</param>
|
||||||
|
/// <param name="baseAddress">The base address that is expected</param>
|
||||||
|
/// <param name="isAuthenticated">Func for checking if the request is authenticated</param>
|
||||||
|
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||||
|
/// <param name="stjCompare">Use System.Text.Json for comparing</param>
|
||||||
|
public RestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null, bool stjCompare = true)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_folder = folder;
|
||||||
|
_baseAddress = baseAddress;
|
||||||
|
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||||
|
_isAuthenticated = isAuthenticated;
|
||||||
|
_stjCompare = stjCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public Task ValidateAsync<TResponse>(
|
||||||
|
Func<TClient, Task<WebCallResult<TResponse>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool useSingleArrayItem = false,
|
||||||
|
bool skipResponseValidation = false)
|
||||||
|
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||||
|
/// <typeparam name="TActualResponse">The concrete response type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync<TResponse, TActualResponse>(
|
||||||
|
Func<TClient, Task<WebCallResult<TResponse>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool useSingleArrayItem = false,
|
||||||
|
bool skipResponseValidation = false) where TActualResponse : TResponse
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception($"Response file not found for {name}: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
var expectedMethod = reader.ReadLine();
|
||||||
|
var expectedPath = reader.ReadLine();
|
||||||
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
|
var response = reader.ReadToEnd();
|
||||||
|
|
||||||
|
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||||
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Check request/response properties
|
||||||
|
if (result.Error != null)
|
||||||
|
throw new Exception(name + " returned error " + result.Error);
|
||||||
|
if (_isAuthenticated(result.AsDataless()) != expectedAuth)
|
||||||
|
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
|
||||||
|
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||||
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
if (!skipResponseValidation)
|
||||||
|
{
|
||||||
|
// Check response data
|
||||||
|
object responseData = (TActualResponse)result.Data!;
|
||||||
|
if (_stjCompare == true)
|
||||||
|
SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||||
|
else
|
||||||
|
JsonNetComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync(
|
||||||
|
Func<TClient, Task<WebCallResult>> methodInvoke,
|
||||||
|
string name)
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception($"Response file not found for {name}: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
var expectedMethod = reader.ReadLine();
|
||||||
|
var expectedPath = reader.ReadLine();
|
||||||
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
|
var response = reader.ReadToEnd();
|
||||||
|
|
||||||
|
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||||
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Check request/response properties
|
||||||
|
if (result.Error != null)
|
||||||
|
throw new Exception(name + " returned error " + result.Error);
|
||||||
|
if (_isAuthenticated(result) != expectedAuth)
|
||||||
|
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}");
|
||||||
|
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||||
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Testing.Comparers;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for websocket subscriptions, checking expected requests and responses and comparing update models
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient"></typeparam>
|
||||||
|
public class SocketSubscriptionValidator<TClient> where TClient : BaseSocketClient
|
||||||
|
{
|
||||||
|
private readonly TClient _client;
|
||||||
|
private readonly string _folder;
|
||||||
|
private readonly string _baseAddress;
|
||||||
|
private readonly string? _nestedPropertyForCompare;
|
||||||
|
private readonly bool _stjCompare;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client to test</param>
|
||||||
|
/// <param name="folder">Folder for json test values</param>
|
||||||
|
/// <param name="baseAddress">The base address that is expected</param>
|
||||||
|
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||||
|
/// <param name="stjCompare">Use System.Text.Json for comparing</param>
|
||||||
|
public SocketSubscriptionValidator(TClient client, string folder, string baseAddress, string? nestedPropertyForCompare = null, bool stjCompare = true)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_folder = folder;
|
||||||
|
_baseAddress = baseAddress;
|
||||||
|
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||||
|
_stjCompare = stjCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUpdate">The expected update type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Subscription method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useFirstUpdateItem">Use the first item of an array update</param>
|
||||||
|
/// <param name="addressPath">Path</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync<TUpdate>(
|
||||||
|
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
string? addressPath = null,
|
||||||
|
bool? useFirstUpdateItem = null)
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file ;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception("Response file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
|
||||||
|
var socket = TestHelpers.ConfigureSocketClient(_client, addressPath == null ? _baseAddress : _baseAddress.AppendPath(addressPath));
|
||||||
|
|
||||||
|
var waiter = new AutoResetEvent(false);
|
||||||
|
string? lastMessage = null;
|
||||||
|
socket.OnMessageSend += (x) =>
|
||||||
|
{
|
||||||
|
lastMessage = x;
|
||||||
|
waiter.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
TUpdate? update = default;
|
||||||
|
// Invoke subscription method
|
||||||
|
var task = methodInvoke(_client, x => { update = x.Data; });
|
||||||
|
|
||||||
|
string? overrideKey = null;
|
||||||
|
string? overrideValue = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var line = reader.ReadLine();
|
||||||
|
if (line == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (line.StartsWith("> "))
|
||||||
|
{
|
||||||
|
// Expect a message from client to server
|
||||||
|
waiter.WaitOne(TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
if (lastMessage == null)
|
||||||
|
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
|
||||||
|
|
||||||
|
|
||||||
|
var lastMessageJson = JToken.Parse(lastMessage);
|
||||||
|
var expectedJson = JToken.Parse(line.Substring(2));
|
||||||
|
foreach(var item in expectedJson)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop && prop.Value is JValue val)
|
||||||
|
{
|
||||||
|
if (val.ToString().StartsWith("|") && val.ToString().EndsWith("|"))
|
||||||
|
{
|
||||||
|
// |x| values are used to replace parts or response messages
|
||||||
|
overrideKey = val.ToString();
|
||||||
|
overrideValue = lastMessageJson[prop.Name]?.Value<string>();
|
||||||
|
}
|
||||||
|
else if (val.ToString() == "-999")
|
||||||
|
{
|
||||||
|
// -999 value is used to replace parts or response messages
|
||||||
|
overrideKey = val.ToString();
|
||||||
|
overrideValue = lastMessageJson[prop.Name]?.Value<decimal>().ToString();
|
||||||
|
}
|
||||||
|
else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true)
|
||||||
|
throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check objects and arrays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (line.StartsWith("< "))
|
||||||
|
{
|
||||||
|
// Expect a message from server to client
|
||||||
|
if (overrideKey != null)
|
||||||
|
{
|
||||||
|
line = line.Replace(overrideKey, overrideValue);
|
||||||
|
overrideKey = null;
|
||||||
|
overrideValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.InvokeMessage(line.Substring(2));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A update message from server to client
|
||||||
|
var compareData = reader.ReadToEnd();
|
||||||
|
socket.InvokeMessage(compareData);
|
||||||
|
|
||||||
|
if (update == null)
|
||||||
|
throw new Exception($"{name} Update send to client did not trigger in update handler");
|
||||||
|
|
||||||
|
if (_stjCompare == true)
|
||||||
|
SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||||
|
else
|
||||||
|
JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _client.UnsubscribeAllAsync().ConfigureAwait(false);
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Testing.Implementations;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Testing helpers
|
||||||
|
/// </summary>
|
||||||
|
public class TestHelpers
|
||||||
|
{
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
internal static bool AreEqual<T>(T? self, T? to, params string[] ignore) where T : class
|
||||||
|
{
|
||||||
|
if (self != null && to != null)
|
||||||
|
{
|
||||||
|
var type = self.GetType();
|
||||||
|
var ignoreList = new List<string>(ignore);
|
||||||
|
foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
if (ignoreList.Contains(pi.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var selfValue = type.GetProperty(pi.Name)!.GetValue(self, null);
|
||||||
|
var toValue = type.GetProperty(pi.Name)!.GetValue(to, null);
|
||||||
|
|
||||||
|
if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll"))
|
||||||
|
{
|
||||||
|
// Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
|
||||||
|
if (AreEqual(selfValue, toValue, ignore))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self == to;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TestSocket ConfigureSocketClient<T>(T client, string address) where T : BaseSocketClient
|
||||||
|
{
|
||||||
|
var socket = new TestSocket(address);
|
||||||
|
foreach (var apiClient in client.ApiClients.OfType<SocketApiClient>())
|
||||||
|
{
|
||||||
|
apiClient.SocketFactory = new TestWebsocketFactory(socket);
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ConfigureRestClient<T>(T client, string data, HttpStatusCode code) where T : BaseRestClient
|
||||||
|
{
|
||||||
|
foreach (var apiClient in client.ApiClients.OfType<RestApiClient>())
|
||||||
|
{
|
||||||
|
var expectedBytes = Encoding.UTF8.GetBytes(data);
|
||||||
|
var responseStream = new MemoryStream();
|
||||||
|
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
|
||||||
|
responseStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var response = new TestResponse(code, responseStream);
|
||||||
|
var request = new TestRequest(response);
|
||||||
|
|
||||||
|
var factory = new TestRequestFactory(request);
|
||||||
|
apiClient.RequestFactory = factory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check a signature matches the expected signature
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="authProvider"></param>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
/// <param name="path"></param>
|
||||||
|
/// <param name="getSignature"></param>
|
||||||
|
/// <param name="expectedSignature"></param>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="time"></param>
|
||||||
|
/// <param name="disableOrdering"></param>
|
||||||
|
/// <param name="compareCase"></param>
|
||||||
|
/// <param name="host"></param>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static void CheckSignature(
|
||||||
|
RestApiClient client,
|
||||||
|
AuthenticationProvider authProvider,
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
Func<IDictionary<string, object>?, IDictionary<string, object>?, IDictionary<string, string>?, string> getSignature,
|
||||||
|
string expectedSignature,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
DateTime? time = null,
|
||||||
|
bool disableOrdering = false,
|
||||||
|
bool compareCase = true,
|
||||||
|
string host = "https://test.test-api.com")
|
||||||
|
{
|
||||||
|
parameters ??= new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "test", 123 },
|
||||||
|
{ "test2", "abc" }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (disableOrdering)
|
||||||
|
client.OrderParameters = false;
|
||||||
|
|
||||||
|
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
|
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
authProvider.AuthenticateRequest(
|
||||||
|
client,
|
||||||
|
new Uri(host.AppendPath(path)),
|
||||||
|
method,
|
||||||
|
uriParams,
|
||||||
|
bodyParams,
|
||||||
|
headers,
|
||||||
|
true,
|
||||||
|
client.ArraySerialization,
|
||||||
|
client.ParameterPositions[method],
|
||||||
|
client.RequestBodyFormat
|
||||||
|
);
|
||||||
|
|
||||||
|
var signature = getSignature(uriParams, bodyParams, headers);
|
||||||
|
|
||||||
|
if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scan the TClient rest client type for missing interface methods
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient"></typeparam>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static void CheckForMissingRestInterfaces<TClient>()
|
||||||
|
{
|
||||||
|
CheckForMissingInterfaces(typeof(TClient), typeof(Task));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scan the TClient socket client type for missing interface methods
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient"></typeparam>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public static void CheckForMissingSocketInterfaces<TClient>()
|
||||||
|
{
|
||||||
|
CheckForMissingInterfaces(typeof(TClient), typeof(Task<CallResult<UpdateSubscription>>));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckForMissingInterfaces(Type clientType, Type implementationTypes)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetAssembly(clientType);
|
||||||
|
var interfaceType = clientType.GetInterface("I" + clientType.Name);
|
||||||
|
var clientInterfaces = assembly.GetTypes().Where(t => t.Name.StartsWith("I" + clientType.Name));
|
||||||
|
|
||||||
|
foreach (var clientInterface in clientInterfaces)
|
||||||
|
{
|
||||||
|
var implementation = assembly.GetTypes().Single(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||||
|
int methods = 0;
|
||||||
|
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
|
||||||
|
{
|
||||||
|
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray());
|
||||||
|
if (interfaceMethod == null)
|
||||||
|
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||||
|
methods++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
[](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) 
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
Note that the CryptoExchange.Net package itself can not be used directly for accessing API's. Either install a client library from the list below or use [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes access to all exchange API's.
|
||||||
|
|
||||||
For more information on what CryptoExchange.Net and it's client libraries offers see the [Documentation](https://jkorf.github.io/CryptoExchange.Net/).
|
For more information on what CryptoExchange.Net and it's client libraries offers see the [Documentation](https://jkorf.github.io/CryptoExchange.Net/).
|
||||||
|
|
||||||
@@ -18,12 +19,15 @@ The following API's are directly supported. Note that there are 3rd party implem
|
|||||||
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|
|
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|
|
||||||
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|
|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|
|
||||||
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|
|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|
|
||||||
|
|Gate.io|[JKorf/GateIo.Net](https://github.com/JKorf/GateIo.Net)|[](https://www.nuget.org/packages/GateIo.Net)|
|
||||||
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[](https://www.nuget.org/packages/Huobi.Net)|
|
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[](https://www.nuget.org/packages/Huobi.Net)|
|
||||||
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[](https://www.nuget.org/packages/KrakenExchange.Net)|
|
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[](https://www.nuget.org/packages/KrakenExchange.Net)|
|
||||||
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|
|
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|
|
||||||
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|
|
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|
|
||||||
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|
|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|
|
||||||
|
|
||||||
|
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
[](https://discord.gg/MSpeEtSY8t)
|
[](https://discord.gg/MSpeEtSY8t)
|
||||||
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
||||||
@@ -42,6 +46,58 @@ 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.7.2 - 25 Jun 2024
|
||||||
|
* Fixed ratelimiting issue possibly creating negative delays
|
||||||
|
|
||||||
|
* Version 7.7.1 - 23 Jun 2024
|
||||||
|
* Fixes for caching implementation
|
||||||
|
|
||||||
|
* Version 7.7.0 - 23 Jun 2024
|
||||||
|
* Caching support
|
||||||
|
* Caching is supported for GET requests within a certain time frame
|
||||||
|
* Enable caching by setting CachingEnabled to true in the client options
|
||||||
|
* Added DataSource to CallResult object
|
||||||
|
* Dedicated websocket connection
|
||||||
|
* Added functionality for always having a connection open which can then be used for order operations
|
||||||
|
* This eliminates the initial connection time for the first request
|
||||||
|
* WebSocket connection can be prepared by calling PrepareConnectionsAsync on the Api client, for example `await binanceSocketClient.SpotApi.PrepareConnectionsAsync()`. This is only needed initially; it will be reconnected when connection is lost.
|
||||||
|
* Added CancellationToken support for websocket queries
|
||||||
|
* Added SocketConnection parameter to SocketApiClient.GetAuthenticationRequest method
|
||||||
|
* Added ObjectStringConverter base converter for deserializing nested json strings
|
||||||
|
* Fixed websocket issue with ratelimiting and reconnecting interaction
|
||||||
|
* Fixed rate limiting issue with sub-millisecond delays
|
||||||
|
* Fixed websocket connection will now close if authentication fails because of not set credentials
|
||||||
|
* Updated websocket reconnection handling and options, added backoff policy
|
||||||
|
* Removed check for confirmed subscription as data often is pushed before the subscription is confirmed
|
||||||
|
|
||||||
|
* Version 7.6.0 - 11 Jun 2024
|
||||||
|
* Added support for specifying seperate uri and body parameters
|
||||||
|
* Added support for different message and handling generic types on socket queries
|
||||||
|
* Added support for PATCH http method requests
|
||||||
|
* Added support for setting http request body to a specific type directly
|
||||||
|
* Split DataEvent.Topic into StreamId and Symbol properties
|
||||||
|
* Added support for negative time values parsing
|
||||||
|
* Added some helper methods for converting DataEvent to CallResult
|
||||||
|
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
|
||||||
|
* Updated some testing methods
|
||||||
|
|
||||||
|
* Version 7.5.2 - 07 May 2024
|
||||||
|
* Fixed SetApiCredentials not correctly being used by rate limiter causing exception
|
||||||
|
|
||||||
|
* Version 7.5.1 - 03 May 2024
|
||||||
|
* Some small improvements in unit testing components
|
||||||
|
|
||||||
|
* Version 7.5.0 - 01 May 2024
|
||||||
|
* Added testing implementations
|
||||||
|
* Small refactor AuthenticationProvider to allow better testing
|
||||||
|
* Change result of MessageAccessor.Read methods to CallResult so error can be returned
|
||||||
|
* Moved some DateTimeConverter logic to seperate methods to allow access from outside converters
|
||||||
|
|
||||||
|
* Version 7.4.0 - 28 Apr 2024
|
||||||
|
* Added FormatSymbol on IBaseApiClient interface
|
||||||
|
* Added IOrderBookFactory interface
|
||||||
|
* Removed ExchangeOptions as base class for OrderBookOptions
|
||||||
|
|
||||||
* Version 7.3.3 - 23 Apr 2024
|
* Version 7.3.3 - 23 Apr 2024
|
||||||
* Added support for new DateTime format parsing
|
* Added support for new DateTime format parsing
|
||||||
* Updated some logging
|
* Updated some logging
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
+238
-80
@@ -82,16 +82,17 @@
|
|||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_installation">Installation</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_installation">Installation</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_di">Dependency Injection</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_di">Dependency Injection</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="#idocs_general">General Client Usage</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_rest">REST API Client</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_rest">REST API Client</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_socket">Websocket API Client</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_socket">Websocket API Client</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_common">Common clients</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_common">Common Clients</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_options">Options & Authorization</a>
|
<li class="nav-item"><a class="nav-link" href="#idocs_options">Options & Authorization</a>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_auth">Authorization</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_auth">Authorization</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_options_set">Setting options</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_options_set">Setting Options</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_options_def">Option definitions</a></li>
|
<li class="nav-item"><a class="nav-link" href="#idocs_options_def">Option Definitions</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"><a class="nav-link" href="#idocs_features">Additional Features</a>
|
<li class="nav-item"><a class="nav-link" href="#idocs_features">Additional Features</a>
|
||||||
@@ -122,35 +123,39 @@
|
|||||||
<div class="idocs-content">
|
<div class="idocs-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section id="idocs_intro">
|
<section id="idocs_intro">
|
||||||
<h1>CryptoExchange.Net</h1>
|
<h1>CryptoExchange.Net & Implementations</h1>
|
||||||
|
|
||||||
<p>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.</p>
|
<p>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.</p>
|
||||||
<div class="alert alert-info">All libraries can be used in the same project as well as indivually, just install the exchange libraries you need!</div>
|
<p>When access to multiple or all exchange API's is needed, the CryptoClients.Net library combines all different client libraries in a single Nuget package. The following image illustrates the structure:</p>
|
||||||
<p>The following API's are directly supported. Note that there are 3rd party implementations going around, but only these are created and supported by me</p>
|
<p><img src="assets/images/struct.png" /></p>
|
||||||
|
<p>These Exchanges/API's are directly supported:</p>
|
||||||
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exchange</th>
|
<th>API</th>
|
||||||
<th>Repository</th>
|
<th>Repository</th>
|
||||||
<th>Nuget</th>
|
<th>Nuget</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr><td>Binance</td><td><a href="https://github.com/JKorf/Binance.Net">JKorf/Binance.Net</a></td><td><a href="https://www.nuget.org/packages/Binance.Net"><img src="https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Binance</td><td><a href="https://github.com/JKorf/Binance.Net">JKorf/Binance.Net</a></td><td><a href="https://www.nuget.org/packages/Binance.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>BingX</td><td><a href="https://github.com/JKorf/BingX.Net">JKorf/BingX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.BingX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Bitfinex</td><td><a href="https://github.com/JKorf/Bitfinex.Net">JKorf/Bitfinex.Net</a></td><td><a href="https://www.nuget.org/packages/Bitfinex.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Bitget</td><td><a href="https://github.com/JKorf/Bitget.Net">JKorf/Bitget.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Bitget.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Bybit</td><td><a href="https://github.com/JKorf/Bybit.Net">JKorf/Bybit.Net</a></td><td><a href="https://www.nuget.org/packages/Bybit.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>CoinEx</td><td><a href="https://github.com/JKorf/CoinEx.Net">JKorf/CoinEx.Net</a></td><td><a href="https://www.nuget.org/packages/CoinEx.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>CoinGecko</td><td><a href="https://github.com/JKorf/CoinGecko.Net">JKorf/CoinGecko.Net</a></td><td><a href="https://www.nuget.org/packages/CoinGecko.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Huobi</td><td><a href="https://github.com/JKorf/Huobi.Net">JKorf/Huobi.Net</a></td><td><a href="https://www.nuget.org/packages/Huobi.Net"><img src="https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Gate.io</td><td><a href="https://github.com/JKorf/GateIo.Net">JKorf/GateIo.Net</a></td><td><a href="https://www.nuget.org/packages/GateIo.Net" target="_blank"><img src="https://img.shields.io/nuget/v/GateIo.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Kraken</td><td><a href="https://github.com/JKorf/Kraken.Net">JKorf/Kraken.Net</a></td><td><a href="https://www.nuget.org/packages/KrakenExchange.Net"><img src="https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Huobi</td><td><a href="https://github.com/JKorf/Huobi.Net">JKorf/Huobi.Net</a></td><td><a href="https://www.nuget.org/packages/Huobi.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Kucoin</td><td><a href="https://github.com/JKorf/Kucoin.Net">JKorf/Kucoin.Net</a></td><td><a href="https://www.nuget.org/packages/Kucoin.Net"><img src="https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Kraken</td><td><a href="https://github.com/JKorf/Kraken.Net">JKorf/Kraken.Net</a></td><td><a href="https://www.nuget.org/packages/KrakenExchange.Net" target="_blank"><img src="https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>Mexc</td><td><a href="https://github.com/JKorf/Mexc.Net">JKorf/Mexc.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Mexc.Net"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Kucoin</td><td><a href="https://github.com/JKorf/Kucoin.Net">JKorf/Kucoin.Net</a></td><td><a href="https://www.nuget.org/packages/Kucoin.Net" target="_blank"><img src="https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square" /></a></td></tr>
|
||||||
<tr><td>OKX</td><td><a href="https://github.com/JKorf/OKX.Net">JKorf/OKX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.OKX.Net"><img src="https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square" /></a></td></tr>
|
<tr><td>Mexc</td><td><a href="https://github.com/JKorf/Mexc.Net">JKorf/Mexc.Net</a></td><td><a href="https://www.nuget.org/packages/JK.Mexc.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square" /></a></td></tr>
|
||||||
|
<tr><td>OKX</td><td><a href="https://github.com/JKorf/OKX.Net">JKorf/OKX.Net</a></td><td><a href="https://www.nuget.org/packages/JK.OKX.Net" target="_blank"><img src="https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square" /></a></td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
<p>Note that there are 3rd party implementations going around, but only the listed ones here are created and supported by me.</p>
|
||||||
|
<p>When using multiple of these API's the <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> package can be used which combines these packages and allows easy access to all exchange API's.</p>
|
||||||
|
|
||||||
<h4>Supported Frameworks</h4>
|
<h4>Supported Frameworks</h4>
|
||||||
<p>
|
<p>
|
||||||
The library is targeting both <code>.NET Standard 2.0</code> and <code>.NET Standard 2.1</code> for optimal compatibility
|
The libraries are targeting both <code>.NET Standard 2.0</code> and <code>.NET Standard 2.1</code> for optimal compatibility
|
||||||
</p>
|
</p>
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -202,11 +207,14 @@
|
|||||||
<section id="idocs_installation">
|
<section id="idocs_installation">
|
||||||
<h2>Installation</h2>
|
<h2>Installation</h2>
|
||||||
|
|
||||||
<p>Add the package via dotnet, or add it via the package manager. Any number of libraries can be installed, just make sure you're always using the latest at that moment.</p>
|
<p>Add the package via dotnet, or add it via the package manager. Any number of libraries can be installed, just make sure you're always using the latest at that moment. Instead of installing all libraries seperately the CryptoClients.Net library can be installed to include all the exchange package in one go.</p>
|
||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="install" role="tablist">
|
<ul class="nav nav-tabs" id="install" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="install-binance-tab" data-toggle="tab" href="#install-binance" role="tab" aria-controls="install-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="install-cc-tab" data-toggle="tab" href="#install-cc" role="tab" aria-controls="install-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="install-binance-tab" data-toggle="tab" href="#install-binance" role="tab" aria-controls="install-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="install-bingx-tab" data-toggle="tab" href="#install-bingx" role="tab" aria-controls="install-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="install-bingx-tab" data-toggle="tab" href="#install-bingx" role="tab" aria-controls="install-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -243,7 +251,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="install-binance" role="tabpanel" aria-labelledby="install-binance-tab">
|
<div class="tab-pane fade show active" id="install-cc" role="tabpanel" aria-labelledby="install-cc-tab">
|
||||||
|
<pre><code>dotnet add package CryptoClients.Net</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade show" id="install-binance" role="tabpanel" aria-labelledby="install-binance-tab">
|
||||||
<pre><code>dotnet add package Binance.Net</code></pre>
|
<pre><code>dotnet add package Binance.Net</code></pre>
|
||||||
<img src="assets/images/BinanceInstall.png" />
|
<img src="assets/images/BinanceInstall.png" />
|
||||||
</div>
|
</div>
|
||||||
@@ -299,13 +310,16 @@
|
|||||||
<section id="idocs_di">
|
<section id="idocs_di">
|
||||||
<h2>Dependency Injection</h2>
|
<h2>Dependency Injection</h2>
|
||||||
<p>
|
<p>
|
||||||
All client libraries support and encourage usage via the Dotnet dependency injection system. Add all necesary services by calling the <code>Add[Library]();</code> extension method on the service collection. <a href="#idocs_options_set">Options</a> for the clients can be passed as parameters.
|
All client libraries support and encourage usage via the Dotnet dependency injection system. Add all necesary services per exchange by calling the <code>Add[Library]();</code> extension method on the service collection, or use <code>AddCryptoClients()</code> to add all exchange services in a single class. <a href="#idocs_options_set">Options</a> for the clients can be passed as parameters.
|
||||||
</p>
|
</p>
|
||||||
<div class="alert alert-info">Using the dependecy injection mechanism also makes sure the HttpClient is used correctly</div>
|
<div class="alert alert-info">Using the dependecy injection mechanism also makes sure the HttpClient is used correctly</div>
|
||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="di" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="di" role="tablist" style="margin-bottom: -16px;">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link active" id="di-cc-tab" data-toggle="tab" href="#di-cc" role="tab" aria-controls="di-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="di-binance-tab" data-toggle="tab" href="#di-binance" role="tab" aria-controls="di-binance" aria-selected="true">Binance</a>
|
<a class="nav-link" id="di-binance-tab" data-toggle="tab" href="#di-binance" role="tab" aria-controls="di-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="di-bingx-tab" data-toggle="tab" href="#di-bingx" role="tab" aria-controls="di-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="di-bingx-tab" data-toggle="tab" href="#di-bingx" role="tab" aria-controls="di-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -342,7 +356,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="di-binance" role="tabpanel" aria-labelledby="di-binance-tab">
|
<div class="tab-pane fade show active" id="di-cc" role="tabpanel" aria-labelledby="di-cc-tab">
|
||||||
|
<pre><code>builder.Services.AddCryptoClients();</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="di-binance" role="tabpanel" aria-labelledby="di-binance-tab">
|
||||||
<pre><code>builder.Services.AddBinance();</code></pre>
|
<pre><code>builder.Services.AddBinance();</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="di-bingx" role="tabpanel" aria-labelledby="di-bingx-tab">
|
<div class="tab-pane fade" id="di-bingx" role="tabpanel" aria-labelledby="di-bingx-tab">
|
||||||
@@ -385,7 +402,10 @@
|
|||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="interfaces" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="interfaces" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="interfaces-binance-tab" data-toggle="tab" href="#interfaces-binance" role="tab" aria-controls="interfaces-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="interfaces-cc-tab" data-toggle="tab" href="#interfaces-cc" role="tab" aria-controls="interfaces-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="interfaces-binance-tab" data-toggle="tab" href="#interfaces-binance" role="tab" aria-controls="interfaces-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="interfaces-bingx-tab" data-toggle="tab" href="#interfaces-bingx" role="tab" aria-controls="interfaces-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="interfaces-bingx-tab" data-toggle="tab" href="#interfaces-bingx" role="tab" aria-controls="interfaces-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -422,7 +442,41 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="interfaces-binance" role="tabpanel" aria-labelledby="interfaces-binance-tab">
|
<div class="tab-pane fade show active" id="interfaces-cc" role="tabpanel" aria-labelledby="interfaces-cc-tab">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<tr><th>Interface</th><th>Description</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>IExchangeRestClient</code></td>
|
||||||
|
<td>The client for accessing all exchanges REST API's</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>IExchangeSocketClient</code></td>
|
||||||
|
<td>The client for accessing all exchanges Websocket API's</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>IExchangeOrderBookFactory</code></td>
|
||||||
|
<td>A factory class for accessing all exchanges SymbolOrderBook (locally synced order books) factory methods</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>I[Library]RestClient</code></td>
|
||||||
|
<td>All exchange specific REST clients, for example <code>IBinanceRestClient</code> and <code>IMexcRestClient</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>I[Library]SocketClient</code></td>
|
||||||
|
<td>All exchange specific Websocket clients, for example <code>IBinanceSocketClient</code> and <code>IMexcSocketClient</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>I[Library]OrderBookFactory</code></td>
|
||||||
|
<td>All exchange specific order book factories. The factory can be used for creating SymbolOrderBook (locally synced order books) instances for the exchange</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ISpotClient</code></td>
|
||||||
|
<td>An implementation of the ISpotClient interface for each exchange. The ISpotClient offers basic Spot API functionality in a combined interface</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
|
<div class="tab-pane fade" id="interfaces-binance" role="tabpanel" aria-labelledby="interfaces-binance-tab">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<tr><th>Interface</th><th>Description</th></tr>
|
<tr><th>Interface</th><th>Description</th></tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -760,6 +814,15 @@
|
|||||||
|
|
||||||
<hr class="divider">
|
<hr class="divider">
|
||||||
|
|
||||||
|
<section id="idocs_general">
|
||||||
|
<h2>General client usage</h2>
|
||||||
|
<p>All clients work with the same principles:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Mandatory parameters are non-nullable while optional parameters are nullable and will have a default value of null.</li>
|
||||||
|
<li>Any operation will return a form of <code>CallResult</code>. This result can and should be check for success. The clients will not throw exceptions.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- HTML Structure
|
<!-- HTML Structure
|
||||||
============================ -->
|
============================ -->
|
||||||
<section id="idocs_rest">
|
<section id="idocs_rest">
|
||||||
@@ -770,7 +833,10 @@
|
|||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="rest" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="rest" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="rest-binance-tab" data-toggle="tab" href="#rest-binance" role="tab" aria-controls="rest-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="rest-cc-tab" data-toggle="tab" href="#rest-cc" role="tab" aria-controls="rest-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="rest-binance-tab" data-toggle="tab" href="#rest-binance" role="tab" aria-controls="rest-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="rest-bingx-tab" data-toggle="tab" href="#rest-bingx" role="tab" aria-controls="rest-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="rest-bingx-tab" data-toggle="tab" href="#rest-bingx" role="tab" aria-controls="rest-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -807,7 +873,19 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="rest-binance" role="tabpanel" aria-labelledby="rest-binance-tab">
|
<div class="tab-pane fade show active" id="rest-cc" role="tabpanel" aria-labelledby="rest-cc-tab">
|
||||||
|
<pre><code>var client = new ExchangeRestClient();
|
||||||
|
var tickersResult = await client.Binance.SpotApi.ExchangeData.GetTickersAsync();
|
||||||
|
if (!tickersResult.Success)
|
||||||
|
{
|
||||||
|
// Handle error, tickersResult.Error contains more information
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle data, tickersResult.Data will contain the actual data
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="rest-binance" role="tabpanel" aria-labelledby="rest-binance-tab">
|
||||||
<pre><code>var client = new BinanceRestClient();
|
<pre><code>var client = new BinanceRestClient();
|
||||||
var tickersResult = await client.SpotApi.ExchangeData.GetTickersAsync();
|
var tickersResult = await client.SpotApi.ExchangeData.GetTickersAsync();
|
||||||
if (!tickersResult.Success)
|
if (!tickersResult.Success)
|
||||||
@@ -1020,11 +1098,16 @@ else
|
|||||||
|
|
||||||
<p>The client can be injected via <a href="#di">dependency injection</a>, or constructed manually. When constructing manually keep in mind that when the client is disposed all connections will get closed as well.</p>
|
<p>The client can be injected via <a href="#di">dependency injection</a>, or constructed manually. When constructing manually keep in mind that when the client is disposed all connections will get closed as well.</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info">The socket client requires a continous connection to the server. The connection can be lost due to internet interuptions, or the server disconnecting the client. This is expected behaviour and the socket client will automatically reconnect when this happens.</div>
|
||||||
|
|
||||||
<h4>Subscribing</h4>
|
<h4>Subscribing</h4>
|
||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="socket" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="socket" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="socket-binance-tab" data-toggle="tab" href="#socket-binance" role="tab" aria-controls="socket-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="socket-cc-tab" data-toggle="tab" href="#socket-cc" role="tab" aria-controls="socket-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="socket-binance-tab" data-toggle="tab" href="#socket-binance" role="tab" aria-controls="socket-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="socket-bingx-tab" data-toggle="tab" href="#socket-bingx" role="tab" aria-controls="socket-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="socket-bingx-tab" data-toggle="tab" href="#socket-bingx" role="tab" aria-controls="socket-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -1058,7 +1141,18 @@ else
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="socket-binance" role="tabpanel" aria-labelledby="socket-binance-tab">
|
<div class="tab-pane fade show active" id="socket-cc" role="tabpanel" aria-labelledby="socket-cc-tab">
|
||||||
|
<pre><code>var client = new ExchangeSocketClient();
|
||||||
|
var subscribeResult = await client.Binance.SpotApi.ExchangeData.SubscribeToAllTickerUpdatesAsync(update => {
|
||||||
|
// Handle the data update, update.Data will contain the actual data
|
||||||
|
});
|
||||||
|
if (!subscribeResult.Success)
|
||||||
|
{
|
||||||
|
// Handle error, subscribeResult.Error contains more information on why the subscription failed
|
||||||
|
}
|
||||||
|
// Subscribing was successfull, the data will now be streamed into the data handler</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="socket-binance" role="tabpanel" aria-labelledby="socket-binance-tab">
|
||||||
<pre><code>var client = new BinanceSocketClient();
|
<pre><code>var client = new BinanceSocketClient();
|
||||||
var subscribeResult = await client.SpotApi.ExchangeData.SubscribeToAllTickerUpdatesAsync(update => {
|
var subscribeResult = await client.SpotApi.ExchangeData.SubscribeToAllTickerUpdatesAsync(update => {
|
||||||
// Handle the data update, update.Data will contain the actual data
|
// Handle the data update, update.Data will contain the actual data
|
||||||
@@ -1294,34 +1388,34 @@ await client.UnsubscribeAllAsync();</code></pre>
|
|||||||
============================ -->
|
============================ -->
|
||||||
<section id="idocs_common">
|
<section id="idocs_common">
|
||||||
<h2>Common Clients</h2>
|
<h2>Common Clients</h2>
|
||||||
<p>CryptoExchange.Net exposes some common clients. These clients aim to make using the different API's easier.</p>
|
<p>The CryptoClients.Net client exposes some common client classes. These clients aim to make using the different API's easier.</p>
|
||||||
|
|
||||||
<p><b>(I)CryptoRestClient</b><br />
|
<p><b>(I)ExchangeRestClient</b><br />
|
||||||
The <code>ICryptoRestClient</code> (or <code>CryptoRestClient</code> when used directly) can be used to easily access REST clients for different API's through the different packages that have been installed. Each package adds it extension method to the interface, which allows the user to access the clients via it.
|
The <code>IExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
For example, having the Binance, Bybit and Kucoin packages installed allows you to use it like this:
|
For example, using the Binance, Bybit and Kucoin API's can be done like this:
|
||||||
<pre><code>var cryptoRestClient = new CryptoRestClient(); // Either construct it or inject the ICryptoRestClient into your service
|
<pre><code>var exchangeRestClient = new ExchangeRestClient(); // Either construct it or inject the IExchangeRestClient into your service after having called 'AddCryptoClients()' during service regirations
|
||||||
var binanceTicker = await cryptoRestClient.Binance().SpotApi.ExchangeData.GetTickersAsync();
|
var binanceTicker = await exchangeRestClient.Binance.SpotApi.ExchangeData.GetTickersAsync();
|
||||||
var bybitTicker = await cryptoRestClient.Bybit().V5Api.ExchangeData.GetTickers();
|
var bybitTicker = await exchangeRestClient.Bybit.V5Api.ExchangeData.GetTickers();
|
||||||
var kucoinTicker = await cryptoRestClient.Kucoin().SpotApi.ExchangeData.GetTickers();</code></pre>
|
var kucoinTicker = await exchangeRestClient.Kucoin.SpotApi.ExchangeData.GetTickers();</code></pre>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><b>(I)CryptoSocketClient</b><br />
|
<p><b>(I)ExchangeSocketClient</b><br />
|
||||||
Similarly as the (I)CryptoRestClient this client allows you to access the different Websocket clients through a single access point.
|
Similarly as the <code>(I)ExchangeRestClient</code> this client allows you to access the different Websocket clients through a single access point.
|
||||||
</p>
|
</p>
|
||||||
<p>Having the Bitget, Kraken and OKX packages installed would allow you to use it like this:
|
<p>For example accessing the Bitget, Kraken and OKX API's could be done like this:
|
||||||
<pre><code>var cryptoRestClient = new CryptoSocketClient(); // Either construct it or inject the ICryptoRestClient into your service
|
<pre><code>var exchangeSocketClient = new ExchangeSocketClient(); // Either construct it or inject the IExchangeSocketClient into your service after having called 'AddCryptoClients()' during service regirations
|
||||||
var bitgetSub = await cryptoRestClient.Bitget().SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {});
|
var bitgetSub = await exchangeSocketClient.Bitget.SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {});
|
||||||
var krakenSub = await cryptoRestClient.Kraken().SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {});
|
var krakenSub = await exchangeSocketClient.Kraken.SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {});
|
||||||
var okxSub = await cryptoRestClient.OKX().UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre>
|
var okxSub = await exchangeSocketClient.OKX.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><b>ISpotClient</b><br />
|
<p><b>ISpotClient</b><br />
|
||||||
The ISpotClient is a REST API client interface implemented by each library which implements a Spot trading API. It provided a common way of doing basic operations on the Spot market, for example getting ticker or trade data, but also placing and retrieving orders. Because this interface is implemented for each exchange with a Spot market the interface is relatively basic, only exposing methods that are supported by all the APIs.
|
The <code>ISpotClient</code> is a REST API client interface implemented by each library which implements a Spot trading API. It provided a common way of doing basic operations on the Spot market, for example getting ticker or trade data, but also placing and retrieving orders. Because this interface is implemented for each exchange with a Spot market the interface is relatively basic, only exposing methods that are supported by all the APIs.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The ISpotClient is added to the service collection when using <a href="#idocs_di">dependency injection</a>. Alternatively it can be accessed for a specific client by calling the `CommonSpotClient` property on the Spot sub-API of a client:
|
The <code>ISpotClient</code> is added to the service collection when using <a href="#idocs_di">dependency injection</a>. Alternatively it can be accessed for a specific client by calling the `CommonSpotClient` property on the Spot sub-API of a client:
|
||||||
<pre><code>var spotClient = restClient.SpotApi.CommonSpotClient;</code></pre>
|
<pre><code>var spotClient = restClient.SpotApi.CommonSpotClient;</code></pre>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -1383,7 +1477,10 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
|||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="options" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="options" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="options-binance-tab" data-toggle="tab" href="#options-binance" role="tab" aria-controls="options-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="options-cc-tab" data-toggle="tab" href="#options-cc" role="tab" aria-controls="options-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="options-binance-tab" data-toggle="tab" href="#options-binance" role="tab" aria-controls="options-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="options-bingx-tab" data-toggle="tab" href="#options-bingx" role="tab" aria-controls="options-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="options-bingx-tab" data-toggle="tab" href="#options-bingx" role="tab" aria-controls="options-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -1420,7 +1517,16 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="options-binance" role="tabpanel" aria-labelledby="options-binance-tab">
|
<div class="tab-pane fade show active" id="options-cc" role="tabpanel" aria-labelledby="options-cc-tab">
|
||||||
|
<pre><code>builder.Services.AddCryptoClients(globalOptions => {
|
||||||
|
globalOptions.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
},
|
||||||
|
// Exchange specific options can be provided as well
|
||||||
|
bybitRestOptions: bybitOptions => {
|
||||||
|
// Set options specific for the Bybit rest client here
|
||||||
|
});</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="options-binance" role="tabpanel" aria-labelledby="options-binance-tab">
|
||||||
<pre><code>builder.Services.AddBinance(
|
<pre><code>builder.Services.AddBinance(
|
||||||
restOptions => {
|
restOptions => {
|
||||||
restOptions.RequestTimeout = TimeSpan.FromSeconds(30);
|
restOptions.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||||
@@ -1533,7 +1639,10 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
|||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="options-constr" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="options-constr" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="options-binance-tab" data-toggle="tab" href="#options-constr-binance" role="tab" aria-controls="options-constr-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="options-cc-tab" data-toggle="tab" href="#options-constr-cc" role="tab" aria-controls="options-constr-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="options-binance-tab" data-toggle="tab" href="#options-constr-binance" role="tab" aria-controls="options-constr-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="options-bingx-tab" data-toggle="tab" href="#options-constr-bingx" role="tab" aria-controls="options-constr-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="options-bingx-tab" data-toggle="tab" href="#options-constr-bingx" role="tab" aria-controls="options-constr-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -1570,7 +1679,13 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="options-constr-binance" role="tabpanel" aria-labelledby="options-binance-tab">
|
<div class="tab-pane fade show active" id="options-constr-cc" role="tabpanel" aria-labelledby="options-cc-tab">
|
||||||
|
<pre><code>var exchangeRestClient = new ExchangeRestClient(opts =>
|
||||||
|
{
|
||||||
|
opts.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
});</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="options-constr-binance" role="tabpanel" aria-labelledby="options-binance-tab">
|
||||||
<pre><code>var binanceRestClient = new BinanceRestClient(opts =>
|
<pre><code>var binanceRestClient = new BinanceRestClient(opts =>
|
||||||
{
|
{
|
||||||
opts.RequestTimeout = TimeSpan.FromSeconds(30);
|
opts.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||||
@@ -1796,7 +1911,7 @@ var client = new OKXRestClient();</code></pre>
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>ApiCredentials</td>
|
<td>ApiCredentials</td>
|
||||||
<td>The credentials to use for private endpoints and streams. See <a href="idocs_auth">Authorization</a> for more info</td>
|
<td>The credentials to use for private endpoints and streams. See <a href="idocs_auth">Authorization</a> for more info. For CryptoClients this option allows the setting of ApiCredentials for all exchanges.</td>
|
||||||
<td><code>null</code></td>
|
<td><code>null</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -2283,7 +2398,10 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
|||||||
<div class="tab-wrap">
|
<div class="tab-wrap">
|
||||||
<ul class="nav nav-tabs" id="limit" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="limit" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="limit-binance-tab" data-toggle="tab" href="#limit-binance" role="tab" aria-controls="limit-binance" aria-selected="true">Binance</a>
|
<a class="nav-link active" id="limit-cc-tab" data-toggle="tab" href="#limit-cc" role="tab" aria-controls="limit-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link" id="limit-binance-tab" data-toggle="tab" href="#limit-binance" role="tab" aria-controls="limit-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="limit-kraken-tab" data-toggle="tab" href="#limit-kraken" role="tab" aria-controls="limit-kraken" aria-selected="false">Kraken</a>
|
<a class="nav-link" id="limit-kraken-tab" data-toggle="tab" href="#limit-kraken" role="tab" aria-controls="limit-kraken" aria-selected="false">Kraken</a>
|
||||||
@@ -2293,7 +2411,18 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="limit-binance" role="tabpanel" aria-labelledby="limit-binance-tab">
|
<div class="tab-pane fade show active" id="limit-cc" role="tabpanel" aria-labelledby="limit-cc-tab">
|
||||||
|
<pre><code>services.AddCryptoClients(x =>
|
||||||
|
x.RatelimiterEnabled = true;
|
||||||
|
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
|
||||||
|
});</code></pre>
|
||||||
|
<p>To be notified of when a rate limit is hit the static <code>Exchanges.RateLimiter</code> exposes an event which triggers when a rate limit is reached</p>
|
||||||
|
<pre><code>BinanceExchange.RateLimiter.RateLimitTriggered += (rateLimitEvent) => Console.WriteLine("Limit triggered: " + rateLimitEvent);
|
||||||
|
|
||||||
|
// Output: Limit triggered: RateLimitEvent { ApiLimit = Spot Socket, LimitDescription = Limit of 6000 per 00:01:00, RequestDefinition = GET 1, Host = wss://ws-api.binance.com, Current = 5752, RequestWeight = 250, Limit = 6000, TimePeriod = 00:01:00, DelayTime = 00:00:38.7784145, Behaviour = Wait }
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="limit-binance" role="tabpanel" aria-labelledby="limit-binance-tab">
|
||||||
<pre><code>services.AddBinance(x =>
|
<pre><code>services.AddBinance(x =>
|
||||||
x.RatelimiterEnabled = true;
|
x.RatelimiterEnabled = true;
|
||||||
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
|
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
|
||||||
@@ -2368,7 +2497,7 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
|||||||
|
|
||||||
<ul class="nav nav-tabs" id="example-symbols" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="example-symbols" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="example-symbols-general-tab" data-toggle="tab" href="#example-symbols-general" role="tab" aria-controls="example-symbols-general" aria-selected="true">CryptoRestClient</a>
|
<a class="nav-link active" id="example-symbols-general-tab" data-toggle="tab" href="#example-symbols-general" role="tab" aria-controls="example-symbols-general" aria-selected="true">CryptoClients</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="example-symbols-binance-tab" data-toggle="tab" href="#example-symbols-binance" role="tab" aria-controls="example-symbols-binance" aria-selected="false">Binance</a>
|
<a class="nav-link" id="example-symbols-binance-tab" data-toggle="tab" href="#example-symbols-binance" role="tab" aria-controls="example-symbols-binance" aria-selected="false">Binance</a>
|
||||||
@@ -2406,9 +2535,8 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="example-symbols-general" role="tabpanel" aria-labelledby="example-symbols-general-tab">
|
<div class="tab-pane fade show active" id="example-symbols-general" role="tabpanel" aria-labelledby="example-symbols-general-tab">
|
||||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
await exchangeRestClient.Binance.SpotApi.ExchangeData.GetExchangeInfoAsync();</code></pre>
|
||||||
await spotClient.GetSymbolsAsync();</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="example-symbols-binance" role="tabpanel" aria-labelledby="example-symbols-binance-tab">
|
<div class="tab-pane fade" id="example-symbols-binance" role="tabpanel" aria-labelledby="example-symbols-binance-tab">
|
||||||
<pre><code>await binanceClient.SpotApi.ExchangeData.GetExchangeInfoAsync();</code></pre>
|
<pre><code>await binanceClient.SpotApi.ExchangeData.GetExchangeInfoAsync();</code></pre>
|
||||||
@@ -2462,7 +2590,7 @@ await spotClient.GetSymbolsAsync();</code></pre>
|
|||||||
|
|
||||||
<ul class="nav nav-tabs" id="example-ticker" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="example-ticker" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="example-ticker-general-tab" data-toggle="tab" href="#example-ticker-general" role="tab" aria-controls="example-ticker-general" aria-selected="true">CryptoRestClient</a>
|
<a class="nav-link active" id="example-ticker-general-tab" data-toggle="tab" href="#example-ticker-general" role="tab" aria-controls="example-ticker-general" aria-selected="true">CryptoClients</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="example-ticker-binance-tab" data-toggle="tab" href="#example-ticker-binance" role="tab" aria-controls="example-ticker-binance" aria-selected="true">Binance</a>
|
<a class="nav-link" id="example-ticker-binance-tab" data-toggle="tab" href="#example-ticker-binance" role="tab" aria-controls="example-ticker-binance" aria-selected="true">Binance</a>
|
||||||
@@ -2500,9 +2628,8 @@ await spotClient.GetSymbolsAsync();</code></pre>
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="example-ticker-general" role="tabpanel" aria-labelledby="example-ticker-general-tab">
|
<div class="tab-pane fade show active" id="example-ticker-general" role="tabpanel" aria-labelledby="example-ticker-general-tab">
|
||||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
await exchangeRestClient.Binance.SpotApi.ExchangeData.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code></pre>
|
||||||
await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="example-ticker-binance" role="tabpanel" aria-labelledby="example-ticker-binance-tab">
|
<div class="tab-pane fade" id="example-ticker-binance" role="tabpanel" aria-labelledby="example-ticker-binance-tab">
|
||||||
<pre><code>await binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");</code></pre>
|
<pre><code>await binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");</code></pre>
|
||||||
@@ -2556,7 +2683,7 @@ await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code>
|
|||||||
|
|
||||||
<ul class="nav nav-tabs" id="example-balances" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="example-balances" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="example-balances-general-tab" data-toggle="tab" href="#example-balances-general" role="tab" aria-controls="example-balances-general" aria-selected="true">CryptoRestClient</a>
|
<a class="nav-link active" id="example-balances-general-tab" data-toggle="tab" href="#example-balances-general" role="tab" aria-controls="example-balances-general" aria-selected="true">CryptoClients</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="example-balances-binance-tab" data-toggle="tab" href="#example-balances-binance" role="tab" aria-controls="example-balances-binance" aria-selected="true">Binance</a>
|
<a class="nav-link" id="example-balances-binance-tab" data-toggle="tab" href="#example-balances-binance" role="tab" aria-controls="example-balances-binance" aria-selected="true">Binance</a>
|
||||||
@@ -2594,9 +2721,8 @@ await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code>
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="example-balances-general" role="tabpanel" aria-labelledby="example-balances-general-tab">
|
<div class="tab-pane fade show active" id="example-balances-general" role="tabpanel" aria-labelledby="example-balances-general-tab">
|
||||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
await exchangeRestClient.Binance.SpotApi.Account.GetBalancesAsync();</code></pre>
|
||||||
await spotClient.GetBalancesAsync();</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="example-balances-binance" role="tabpanel" aria-labelledby="example-balances-binance-tab">
|
<div class="tab-pane fade" id="example-balances-binance" role="tabpanel" aria-labelledby="example-balances-binance-tab">
|
||||||
<pre><code>await binanceClient.SpotApi.Account.GetBalancesAsync();</code></pre>
|
<pre><code>await binanceClient.SpotApi.Account.GetBalancesAsync();</code></pre>
|
||||||
@@ -2654,7 +2780,7 @@ var result = await huobiClient.SpotApi.Account.GetBalancesAsync();</code></pre>
|
|||||||
|
|
||||||
<ul class="nav nav-tabs" id="example-place" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="example-place" role="tablist" style="margin-bottom: -16px;">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="example-place-general-tab" data-toggle="tab" href="#example-place-general" role="tab" aria-controls="example-place-general" aria-selected="true">CryptoRestClient</a>
|
<a class="nav-link active" id="example-place-general-tab" data-toggle="tab" href="#example-place-general" role="tab" aria-controls="example-place-general" aria-selected="true">CryptoClients</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="example-place-binance-tab" data-toggle="tab" href="#example-place-binance" role="tab" aria-controls="example-place-binance" aria-selected="true">Binance</a>
|
<a class="nav-link" id="example-place-binance-tab" data-toggle="tab" href="#example-place-binance" role="tab" aria-controls="example-place-binance" aria-selected="true">Binance</a>
|
||||||
@@ -2692,9 +2818,8 @@ var result = await huobiClient.SpotApi.Account.GetBalancesAsync();</code></pre>
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="example-place-general" role="tabpanel" aria-labelledby="example-place-general-tab">
|
<div class="tab-pane fade show active" id="example-place-general" role="tabpanel" aria-labelledby="example-place-general-tab">
|
||||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
await exchangeRestClient.Binance.SpotApi.Trading.PlaceOrderAsync("BTCUSDT", OrderSide.Buy, SpotOrderType.Limit, 0.1m, price: 50000, timeInForce: TimeInForce.GoodTillCanceled);</code></pre>
|
||||||
await spotClient.PlaceOrderAsync(spotClient.GetSymbolName("BTC", "USDT"), CommonOrderSide.Buy, CommonOrderType.Limit, 0.1m, price: 50000);</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="example-place-binance" role="tabpanel" aria-labelledby="example-place-binance-tab">
|
<div class="tab-pane fade" id="example-place-binance" role="tabpanel" aria-labelledby="example-place-binance-tab">
|
||||||
<pre><code>await binanceClient.SpotApi.Trading.PlaceOrderAsync("BTCUSDT", OrderSide.Buy, SpotOrderType.Limit, 0.1m, price: 50000, timeInForce: TimeInForce.GoodTillCanceled);</code></pre>
|
<pre><code>await binanceClient.SpotApi.Trading.PlaceOrderAsync("BTCUSDT", OrderSide.Buy, SpotOrderType.Limit, 0.1m, price: 50000, timeInForce: TimeInForce.GoodTillCanceled);</code></pre>
|
||||||
@@ -2751,8 +2876,11 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
|||||||
<div>
|
<div>
|
||||||
|
|
||||||
<ul class="nav nav-tabs" id="example-stream-ticker" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="example-stream-ticker" role="tablist" style="margin-bottom: -16px;">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link active" id="example-stream-ticker-cc-tab" data-toggle="tab" href="#example-stream-ticker-cc" role="tab" aria-controls="example-stream-ticker-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="example-stream-ticker-binance-tab" data-toggle="tab" href="#example-stream-ticker-binance" role="tab" aria-controls="example-stream-ticker-binance" aria-selected="true">Binance</a>
|
<a class="nav-link" id="example-stream-ticker-binance-tab" data-toggle="tab" href="#example-stream-ticker-binance" role="tab" aria-controls="example-stream-ticker-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="example-stream-ticker-bingx-tab" data-toggle="tab" href="#example-stream-ticker-bingx" role="tab" aria-controls="example-stream-ticker-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="example-stream-ticker-bingx-tab" data-toggle="tab" href="#example-stream-ticker-bingx" role="tab" aria-controls="example-stream-ticker-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -2786,7 +2914,13 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
<div class="tab-pane fade show active" id="example-stream-ticker-binance" role="tabpanel" aria-labelledby="example-stream-ticker-binance-tab">
|
<div class="tab-pane fade show active" id="example-stream-ticker-cc" role="tabpanel" aria-labelledby="example-stream-ticker-cc-tab">
|
||||||
|
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||||
|
await exchangeSocketClient.Binance.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {
|
||||||
|
// Handle update
|
||||||
|
});</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="example-stream-ticker-binance" role="tabpanel" aria-labelledby="example-stream-ticker-binance-tab">
|
||||||
<pre><code>await binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {
|
<pre><code>await binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {
|
||||||
// Handle update
|
// Handle update
|
||||||
});</code></pre>
|
});</code></pre>
|
||||||
@@ -2860,8 +2994,11 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
|||||||
<div>
|
<div>
|
||||||
|
|
||||||
<ul class="nav nav-tabs" id="example-stream-order" role="tablist" style="margin-bottom: -16px;">
|
<ul class="nav nav-tabs" id="example-stream-order" role="tablist" style="margin-bottom: -16px;">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a class="nav-link active" id="example-stream-order-cc-tab" data-toggle="tab" href="#example-stream-order-cc" role="tab" aria-controls="example-stream-order-cc" aria-selected="true">CryptoClients</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link active" id="example-stream-order-binance-tab" data-toggle="tab" href="#example-stream-order-binance" role="tab" aria-controls="example-stream-order-binance" aria-selected="true">Binance</a>
|
<a class="nav-link" id="example-stream-order-binance-tab" data-toggle="tab" href="#example-stream-order-binance" role="tab" aria-controls="example-stream-order-binance" aria-selected="true">Binance</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" id="example-stream-order-bitfinex-tab" data-toggle="tab" href="#example-stream-order-bingx" role="tab" aria-controls="example-stream-order-bingx" aria-selected="false">BingX</a>
|
<a class="nav-link" id="example-stream-order-bitfinex-tab" data-toggle="tab" href="#example-stream-order-bingx" role="tab" aria-controls="example-stream-order-bingx" aria-selected="false">BingX</a>
|
||||||
@@ -2895,6 +3032,27 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3" id="myTabContent">
|
<div class="tab-content my-3" id="myTabContent">
|
||||||
|
<div class="tab-pane fade show active" id="example-stream-order-cc" role="tabpanel" aria-labelledby="example-stream-order-cc-tab">
|
||||||
|
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||||
|
|
||||||
|
// Retrieve the listen key
|
||||||
|
var listenKey = await exchangeRestClient.Binance.SpotApi.Account.StartUserStreamAsync();
|
||||||
|
|
||||||
|
// Subscribe using the key
|
||||||
|
await exchangeSocketClient.Binance.SpotApi.Account.SubscribeToUserDataUpdatesAsync(listenKey.Data, data => {
|
||||||
|
// Handle update
|
||||||
|
}, null, null, null);
|
||||||
|
|
||||||
|
// The listen key will stay valid for 60 minutes, after this no updates will be send anymore
|
||||||
|
// To extend the life time of the listen key it is recommended to call the KeepAliveUserStreamAsync method every 30 minutes
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Task.Delay(Timespan.FromMinutes(30));
|
||||||
|
await exchangeRestClient.Binance.SpotApi.Account.KeepAliveUserStreamAsync(listenKey.Data);
|
||||||
|
}
|
||||||
|
});</code></pre>
|
||||||
|
</div>
|
||||||
<div class="tab-pane fade show active" id="example-stream-order-binance" role="tabpanel" aria-labelledby="example-stream-order-binance-tab">
|
<div class="tab-pane fade show active" id="example-stream-order-binance" role="tabpanel" aria-labelledby="example-stream-order-binance-tab">
|
||||||
<pre><code>// Retrieve the listen key
|
<pre><code>// Retrieve the listen key
|
||||||
var listenKey = await binanceClient.SpotApi.Account.StartUserStreamAsync();
|
var listenKey = await binanceClient.SpotApi.Account.StartUserStreamAsync();
|
||||||
@@ -3018,17 +3176,17 @@ _ = Task.Run(async () => {
|
|||||||
|
|
||||||
</p>
|
</p>
|
||||||
<pre><code class="language-csharp">using CryptoExchange.Net.Interfaces;
|
<pre><code class="language-csharp">using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoClients.Net.Enums;
|
||||||
|
using CryptoClients.Net.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddBitfinex();
|
builder.Services.AddCryptoClients();
|
||||||
builder.Services.AddBitget();
|
|
||||||
builder.Services.AddKraken();
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapGet("Ticker/{exchange}/{baseAsset}/{quoteAsset}", async ([FromServices] ICryptoRestClient client, string exchange, string baseAsset, string quoteAsset) =>
|
app.MapGet("Ticker/{exchange}/{baseAsset}/{quoteAsset}", async ([FromServices] IExchangeRestClient client, Exchange exchange, string baseAsset, string quoteAsset) =>
|
||||||
{
|
{
|
||||||
var spotClient = client.SpotClient(exchange)!;
|
var spotClient = client.GetUnifiedSpotClient(exchange)!;
|
||||||
var result = await spotClient.GetTickerAsync(spotClient.GetSymbolName(baseAsset, quoteAsset));
|
var result = await spotClient.GetTickerAsync(spotClient.GetSymbolName(baseAsset, quoteAsset));
|
||||||
return result.Data;
|
return result.Data;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user