mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d54e2f9a | |||
| ba3975993f | |||
| 050286ecd1 | |||
| 96c9a55c48 | |||
| a20cbb2f1c | |||
| 2e957d7d9e | |||
| 18d0341056 | |||
| a2bfed2433 | |||
| 67299338a8 | |||
| 971c049c5f |
@@ -54,6 +54,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
return deserializeResult;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
public override TimeSpan? GetTimeOffset() => null;
|
||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||
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();
|
||||
|
||||
@@ -137,6 +137,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
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)
|
||||
{
|
||||
return CreateSocket(address);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -15,6 +16,8 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
{
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
@@ -44,7 +47,6 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for 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="arraySerialization">Array serialization type</param>
|
||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||
@@ -56,14 +58,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object> providedParameters,
|
||||
IDictionary<string, object> uriParameters,
|
||||
IDictionary<string, object> bodyParameters,
|
||||
Dictionary<string, string> headers,
|
||||
bool auth,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
RequestBodyFormat requestBodyFormat,
|
||||
out SortedDictionary<string, object> uriParameters,
|
||||
out SortedDictionary<string, object> bodyParameters,
|
||||
out Dictionary<string, string> headers
|
||||
RequestBodyFormat requestBodyFormat
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@@ -418,9 +419,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <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>
|
||||
@@ -428,7 +429,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
@@ -78,6 +78,9 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
|
||||
@@ -40,23 +40,33 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Request body content type
|
||||
/// </summary>
|
||||
protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
protected string RequestBodyEmptyContent = "{}";
|
||||
protected internal string RequestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
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>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// </summary>
|
||||
@@ -269,22 +279,9 @@ namespace CryptoExchange.Net.Clients
|
||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
|
||||
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 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>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
if (AuthenticationProvider != null)
|
||||
{
|
||||
try
|
||||
@@ -293,14 +290,13 @@ namespace CryptoExchange.Net.Clients
|
||||
this,
|
||||
uri,
|
||||
definition.Method,
|
||||
parameters,
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
headers,
|
||||
definition.Authenticated,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
bodyFormat,
|
||||
out uriParameters,
|
||||
out bodyParameters,
|
||||
out headers);
|
||||
bodyFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -346,7 +342,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
if (bodyParameters.Count != 0)
|
||||
WriteParamBody(request, bodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||
@@ -603,7 +599,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!valid)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -726,8 +722,8 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
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>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
|
||||
if (AuthenticationProvider != null)
|
||||
{
|
||||
try
|
||||
@@ -736,14 +732,13 @@ namespace CryptoExchange.Net.Clients
|
||||
this,
|
||||
uri,
|
||||
method,
|
||||
parameters,
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
headers,
|
||||
signed,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
bodyFormat,
|
||||
out uriParameters,
|
||||
out bodyParameters,
|
||||
out headers);
|
||||
bodyFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -804,7 +799,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="request">The request to set the parameters on</param>
|
||||
/// <param name="parameters">The parameters to set</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)
|
||||
{
|
||||
@@ -859,6 +854,19 @@ namespace CryptoExchange.Net.Clients
|
||||
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>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -38,14 +39,8 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
var longValue = (long)reader.Value;
|
||||
if (longValue == 0 || longValue == -1)
|
||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||
if (longValue < 19999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
|
||||
return ParseFromLong(longValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonToken.Float)
|
||||
{
|
||||
@@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
}
|
||||
|
||||
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: " + 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);
|
||||
return ParseFromString(stringValue);
|
||||
}
|
||||
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>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
@@ -252,14 +252,15 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
|
||||
return IsJson;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
@@ -290,7 +291,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Read(ReadOnlyMemory<byte> data)
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
@@ -305,14 +306,14 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
_token = JToken.Load(jsonTextReader);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
|
||||
return IsJson;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
@@ -49,14 +50,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
var longValue = reader.GetDouble();
|
||||
if (longValue == 0 || longValue == -1)
|
||||
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)
|
||||
{
|
||||
@@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return default;
|
||||
}
|
||||
|
||||
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);
|
||||
return ParseFromString(stringValue!);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -160,6 +86,102 @@ 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 < 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>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
@@ -211,15 +211,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
|
||||
return IsJson;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
{
|
||||
@@ -249,7 +250,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Read(ReadOnlyMemory<byte> data)
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
@@ -257,14 +258,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
_document = JsonDocument.Parse(data);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
|
||||
return IsJson;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>7.3.3</PackageVersion>
|
||||
<AssemblyVersion>7.3.3</AssemblyVersion>
|
||||
<FileVersion>7.3.3</FileVersion>
|
||||
<PackageVersion>7.5.0</PackageVersion>
|
||||
<AssemblyVersion>7.5.0</AssemblyVersion>
|
||||
<FileVersion>7.5.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -342,7 +342,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <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();
|
||||
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>
|
||||
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>
|
||||
/// Set the API credentials for this API client
|
||||
/// </summary>
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="bufferStream"></param>
|
||||
Task<bool> Read(Stream stream, bool bufferStream);
|
||||
Task<CallResult> Read(Stream stream, bool bufferStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -96,6 +96,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Load a data message
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
bool Read(ReadOnlyMemory<byte> data);
|
||||
CallResult Read(ReadOnlyMemory<byte> data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
internal class AuthTimeProvider : IAuthTimeProvider
|
||||
{
|
||||
public DateTime GetTime() => DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : ExchangeOptions
|
||||
public class OrderBookOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
@@ -19,11 +19,7 @@
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -729,7 +729,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// The request weight
|
||||
/// </summary>
|
||||
public int Weight { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
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;
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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 (jsonValue.Value<bool>() != (bool)objectValue)
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
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)
|
||||
// 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;
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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 (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,50 @@
|
||||
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 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)
|
||||
{
|
||||
}
|
||||
|
||||
public Dictionary<string, IEnumerable<string>> GetHeaders() => new();
|
||||
|
||||
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,81 @@
|
||||
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 => new("wss://test.com/ws");
|
||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
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,190 @@
|
||||
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()!);
|
||||
|
||||
TestHelpers.ConfigureRestClient(_client, "", 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,163 @@
|
||||
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>
|
||||
/// <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)
|
||||
{
|
||||
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);
|
||||
|
||||
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 (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);
|
||||
else
|
||||
JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties);
|
||||
}
|
||||
}
|
||||
|
||||
await _client.UnsubscribeAllAsync().ConfigureAwait(false);
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
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) where T : BaseSocketClient
|
||||
{
|
||||
var socket = new TestSocket();
|
||||
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) 
|
||||
|
||||
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/).
|
||||
|
||||
@@ -24,6 +25,8 @@ The following API's are directly supported. Note that there are 3rd party implem
|
||||
|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)|
|
||||
|
||||
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
|
||||
|
||||
## Discord
|
||||
[](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.
|
||||
@@ -42,6 +45,17 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 7.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
|
||||
* Added support for new DateTime format parsing
|
||||
* Updated some logging
|
||||
|
||||
+207
-61
@@ -125,7 +125,7 @@
|
||||
<h1>CryptoExchange.Net</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>
|
||||
<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>
|
||||
<div class="alert alert-info">All libraries can be used in the same project as well as individually, just install the exchange libraries you need!</div>
|
||||
<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>
|
||||
|
||||
<table class="table table-bordered">
|
||||
@@ -148,6 +148,8 @@
|
||||
<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>
|
||||
</table>
|
||||
|
||||
<p>Alternatively, use <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> which combines these packages and allows easy access to all exchange API's.</p>
|
||||
|
||||
<h4>Supported Frameworks</h4>
|
||||
<p>
|
||||
The library is targeting both <code>.NET Standard 2.0</code> and <code>.NET Standard 2.1</code> for optimal compatibility
|
||||
@@ -202,11 +204,14 @@
|
||||
<section id="idocs_installation">
|
||||
<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">
|
||||
<ul class="nav nav-tabs" id="install" role="tablist">
|
||||
<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 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>
|
||||
@@ -243,7 +248,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
<img src="assets/images/BinanceInstall.png" />
|
||||
</div>
|
||||
@@ -299,13 +307,16 @@
|
||||
<section id="idocs_di">
|
||||
<h2>Dependency Injection</h2>
|
||||
<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>
|
||||
<div class="alert alert-info">Using the dependecy injection mechanism also makes sure the HttpClient is used correctly</div>
|
||||
<div class="tab-wrap">
|
||||
<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">
|
||||
<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 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>
|
||||
@@ -342,7 +353,10 @@
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="di-bingx" role="tabpanel" aria-labelledby="di-bingx-tab">
|
||||
@@ -385,7 +399,10 @@
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="interfaces" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -422,7 +439,41 @@
|
||||
</li>
|
||||
</ul>
|
||||
<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">
|
||||
<tr><th>Interface</th><th>Description</th></tr>
|
||||
<tr>
|
||||
@@ -770,7 +821,10 @@
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="rest" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -807,7 +861,19 @@
|
||||
</li>
|
||||
</ul>
|
||||
<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();
|
||||
var tickersResult = await client.SpotApi.ExchangeData.GetTickersAsync();
|
||||
if (!tickersResult.Success)
|
||||
@@ -1020,11 +1086,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>
|
||||
|
||||
<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>
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="socket" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -1058,7 +1129,18 @@ else
|
||||
</li>
|
||||
</ul>
|
||||
<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();
|
||||
var subscribeResult = await client.SpotApi.ExchangeData.SubscribeToAllTickerUpdatesAsync(update => {
|
||||
// Handle the data update, update.Data will contain the actual data
|
||||
@@ -1294,34 +1376,34 @@ await client.UnsubscribeAllAsync();</code></pre>
|
||||
============================ -->
|
||||
<section id="idocs_common">
|
||||
<h2>Common Clients</h2>
|
||||
<p>CryptoExchange.Net exposes some common clients. These clients aim to make using the different API's easier.</p>
|
||||
<p>CryptoClients.Net exposes some common clients. These clients aim to make using the different API's easier.</p>
|
||||
|
||||
<p><b>(I)CryptoRestClient</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.
|
||||
<p><b>(I)ExchangeRestClient</b><br />
|
||||
The <code>ExchangeRestClient</code> (or <code>ExchangeRestClient</code> when used directly) can be used to easily access REST clients for different API's.
|
||||
</p>
|
||||
<p>
|
||||
For example, having the Binance, Bybit and Kucoin packages installed allows you to use it like this:
|
||||
<pre><code>var cryptoRestClient = new CryptoRestClient(); // Either construct it or inject the ICryptoRestClient into your service
|
||||
var binanceTicker = await cryptoRestClient.Binance().SpotApi.ExchangeData.GetTickersAsync();
|
||||
var bybitTicker = await cryptoRestClient.Bybit().V5Api.ExchangeData.GetTickers();
|
||||
var kucoinTicker = await cryptoRestClient.Kucoin().SpotApi.ExchangeData.GetTickers();</code></pre>
|
||||
For example, using the Binance, Bybit and Kucoin API's can be done like this:
|
||||
<pre><code>var exchangeRestClient = new ExchangeRestClient(); // Either construct it or inject the IExchangeRestClient into your service
|
||||
var binanceTicker = await exchangeRestClient.Binance.SpotApi.ExchangeData.GetTickersAsync();
|
||||
var bybitTicker = await exchangeRestClient.Bybit.V5Api.ExchangeData.GetTickers();
|
||||
var kucoinTicker = await exchangeRestClient.Kucoin.SpotApi.ExchangeData.GetTickers();</code></pre>
|
||||
</p>
|
||||
|
||||
<p><b>(I)CryptoSocketClient</b><br />
|
||||
Similarly as the (I)CryptoRestClient this client allows you to access the different Websocket clients through a single access point.
|
||||
<p><b>(I)ExchangeSocketClient</b><br />
|
||||
Similarly as the <code>(I)ExchangeRestClient</code> this client allows you to access the different Websocket clients through a single access point.
|
||||
</p>
|
||||
<p>Having the Bitget, Kraken and OKX packages installed would allow you to use it like this:
|
||||
<pre><code>var cryptoRestClient = new CryptoSocketClient(); // Either construct it or inject the ICryptoRestClient into your service
|
||||
var bitgetSub = await cryptoRestClient.Bitget().SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {});
|
||||
var krakenSub = await cryptoRestClient.Kraken().SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {});
|
||||
var okxSub = await cryptoRestClient.OKX().UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre>
|
||||
<p>For example accessing the Bitget, Kraken and OKX API's could be done like this:
|
||||
<pre><code>var exchangeSocketClient = new ExchangeSocketClient(); // Either construct it or inject the ExchangeSocketClient into your service
|
||||
var bitgetSub = await exchangeSocketClient.Bitget.SpotApi.SubscribeToTickerUpdatesAsync("ETHUSDT", data => {});
|
||||
var krakenSub = await exchangeSocketClient.Kraken.SpotApi.SubscribeToTickerUpdatesAsync("ETH/USD", data => {});
|
||||
var okxSub = await exchangeSocketClient.OKX.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-USDT", data => {});</code></pre>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
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>
|
||||
</p>
|
||||
</section>
|
||||
@@ -1383,7 +1465,10 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="options" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -1420,7 +1505,16 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
||||
</li>
|
||||
</ul>
|
||||
<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(
|
||||
restOptions => {
|
||||
restOptions.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||
@@ -1533,7 +1627,10 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="options-constr" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -1570,7 +1667,13 @@ options.ApiCredentials = new ApiCredentials("YOUR PUBLIC KEY", "YOUR PRIVATE KEY
|
||||
</li>
|
||||
</ul>
|
||||
<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 =>
|
||||
{
|
||||
opts.RequestTimeout = TimeSpan.FromSeconds(30);
|
||||
@@ -1796,7 +1899,7 @@ var client = new OKXRestClient();</code></pre>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -2283,7 +2386,10 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="limit" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -2293,7 +2399,18 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
||||
</li>
|
||||
</ul>
|
||||
<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 =>
|
||||
x.RatelimiterEnabled = true;
|
||||
x.RateLimitingBehaviour = RateLimitingBehaviour.Wait;
|
||||
@@ -2368,7 +2485,7 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
||||
|
||||
<ul class="nav nav-tabs" id="example-symbols" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -2406,9 +2523,8 @@ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options
|
||||
</ul>
|
||||
<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">
|
||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
||||
await spotClient.GetSymbolsAsync();</code></pre>
|
||||
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||
await exchangeRestClient.Binance.SpotApi.ExchangeData.GetExchangeInfoAsync();</code></pre>
|
||||
</div>
|
||||
<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>
|
||||
@@ -2462,7 +2578,7 @@ await spotClient.GetSymbolsAsync();</code></pre>
|
||||
|
||||
<ul class="nav nav-tabs" id="example-ticker" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -2500,9 +2616,8 @@ await spotClient.GetSymbolsAsync();</code></pre>
|
||||
</ul>
|
||||
<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">
|
||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
||||
await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code></pre>
|
||||
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||
await exchangeRestClient.Binance.SpotApi.ExchangeData.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code></pre>
|
||||
</div>
|
||||
<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>
|
||||
@@ -2556,7 +2671,7 @@ await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code>
|
||||
|
||||
<ul class="nav nav-tabs" id="example-balances" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -2594,9 +2709,8 @@ await spotClient.GetTickerAsync(spotClient.GetSymbolName("BTC", "USDT"));</code>
|
||||
</ul>
|
||||
<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">
|
||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
||||
await spotClient.GetBalancesAsync();</code></pre>
|
||||
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||
await exchangeRestClient.Binance.SpotApi.Account.GetBalancesAsync();</code></pre>
|
||||
</div>
|
||||
<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>
|
||||
@@ -2654,7 +2768,7 @@ var result = await huobiClient.SpotApi.Account.GetBalancesAsync();</code></pre>
|
||||
|
||||
<ul class="nav nav-tabs" id="example-place" role="tablist" style="margin-bottom: -16px;">
|
||||
<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 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>
|
||||
@@ -2692,9 +2806,8 @@ var result = await huobiClient.SpotApi.Account.GetBalancesAsync();</code></pre>
|
||||
</ul>
|
||||
<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">
|
||||
<pre><code>// Name of the exchange can be whatever exchange library you have installed, for example Binance, Bybit, Kraken etc
|
||||
var spotClient = cryptoRestClient.SpotClient("[Name of the exchange]");
|
||||
await spotClient.PlaceOrderAsync(spotClient.GetSymbolName("BTC", "USDT"), CommonOrderSide.Buy, CommonOrderType.Limit, 0.1m, price: 50000);</code></pre>
|
||||
<pre><code>// This example uses Binance, but can be any exchange support. For example Bybit, Kraken, Kucoin etc
|
||||
await exchangeRestClient.Binance.SpotApi.Trading.PlaceOrderAsync("BTCUSDT", OrderSide.Buy, SpotOrderType.Limit, 0.1m, price: 50000, timeInForce: TimeInForce.GoodTillCanceled);</code></pre>
|
||||
</div>
|
||||
<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>
|
||||
@@ -2751,8 +2864,11 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
||||
<div>
|
||||
|
||||
<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">
|
||||
<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 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>
|
||||
@@ -2786,7 +2902,13 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
||||
</li>
|
||||
</ul>
|
||||
<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 => {
|
||||
// Handle update
|
||||
});</code></pre>
|
||||
@@ -2860,8 +2982,11 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
||||
<div>
|
||||
|
||||
<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">
|
||||
<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 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>
|
||||
@@ -2895,6 +3020,27 @@ var result = await huobiClient.SpotApi.Trading.PlaceOrderAsync(account.Id, "BTCU
|
||||
</li>
|
||||
</ul>
|
||||
<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">
|
||||
<pre><code>// Retrieve the listen key
|
||||
var listenKey = await binanceClient.SpotApi.Account.StartUserStreamAsync();
|
||||
@@ -3018,17 +3164,17 @@ _ = Task.Run(async () => {
|
||||
|
||||
</p>
|
||||
<pre><code class="language-csharp">using CryptoExchange.Net.Interfaces;
|
||||
using CryptoClients.Net.Enums;
|
||||
using CryptoClients.Net.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddBitfinex();
|
||||
builder.Services.AddBitget();
|
||||
builder.Services.AddKraken();
|
||||
builder.Services.AddCryptoClients();
|
||||
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));
|
||||
return result.Data;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user