1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2026-04-07 10:11:10 +00:00

Compare commits

..

No commits in common. "master" and "CryptoExchange.Net.11.0.1" have entirely different histories.

23 changed files with 107 additions and 450 deletions

View File

@ -1,5 +1,4 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
using NUnit.Framework;
@ -55,63 +54,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(options.ApiCredentials.Secret == "456");
}
[Test]
public void TestSetOptionsRest()
{
var client = new TestRestClient();
client.SetOptions(new UpdateOptions
{
RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
});
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
}
[Test]
public void TestSetOptionsRestWithCredentials()
{
var client = new TestRestClient();
client.SetOptions(new UpdateOptions<HMACCredential>
{
ApiCredentials = new HMACCredential("123", "456"),
RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
});
Assert.That(client.Api1.ApiCredentials, Is.Not.Null);
Assert.That(client.Api1.ApiCredentials.Key, Is.EqualTo("123"));
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
}
[Test]
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
{
TestClientOptions.Default = new TestClientOptions
{
ApiCredentials = new HMACCredential("111", "222"),
RequestTimeout = TimeSpan.FromSeconds(1),
};
var client1 = new TestRestClient();
Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
Assert.That(client1.ClientOptions.ApiCredentials.Key, Is.EqualTo("111"));
TestClientOptions.Default.ApiCredentials = new HMACCredential("333", "444");
TestClientOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
var client2 = new TestRestClient();
Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(client2.ClientOptions.ApiCredentials.Key, Is.EqualTo("333"));
}
}
public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>

View File

@ -1,15 +1,11 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing;
using Newtonsoft.Json.Linq;
using System.Text.Json;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests
{
@ -189,31 +185,6 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result == expected);
}
[Test]
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
{
LibraryHelpers.StaticLogger = new TraceLogger();
var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener);
EnumConverter<TestEnum>.Reset();
try
{
Assert.Throws<Exception>(() =>
{
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
});
Assert.DoesNotThrow(() =>
{
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
});
}
finally
{
Trace.Listeners.Remove(listener);
}
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]

View File

@ -23,7 +23,7 @@ using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestRestClient: BaseRestClient<TestEnvironment, HMACCredential>
public class TestRestClient: BaseRestClient
{
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{
Initialize(options.Value);
Api1 = AddApiClient(new TestRestApi1Client(options.Value));
Api2 = AddApiClient(new TestRestApi2Client(options.Value));
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)

View File

@ -14,11 +14,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient
{
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
/// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
@ -34,14 +29,6 @@ namespace CryptoExchange.Net.Clients
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
@ -54,11 +41,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
/// <summary>
/// Provided client options
/// </summary>
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>

View File

@ -21,11 +21,6 @@ namespace CryptoExchange.Net.Clients
{
#region fields
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
/// <summary>
/// If client is disposing
/// </summary>
@ -140,15 +135,6 @@ namespace CryptoExchange.Net.Clients
return result;
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
@ -161,11 +147,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
/// <summary>
/// Provided client options
/// </summary>
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>

View File

@ -819,16 +819,6 @@ namespace CryptoExchange.Net.Clients
&& definition.Method == HttpMethod.Get
&& !definition.PreventCaching;
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions options)
{
_proxyConfigured = options.Proxy != null;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
}
/// <inheritdoc />
@ -900,9 +890,12 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
base.SetOptions(options);
_proxyConfigured = options.Proxy != null;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
}

View File

@ -1035,28 +1035,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <returns></returns>
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions options)
{
var previousProxyIsSet = _proxyConfigured;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
_proxyConfigured = options.Proxy != null;
if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
}
/// <inheritdoc />
@ -1124,7 +1102,25 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
base.SetOptions(options);
var previousProxyIsSet = _proxyConfigured;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
_proxyConfigured = options.Proxy != null;
if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
}

View File

@ -120,14 +120,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyStringOrNull);
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
if (t != null)
return t.Value;
if (isEmptyStringOrNull && !_unknownValuesWarned.Contains(null))
if (isEmptyString && !_unknownValuesWarned.Contains(null))
{
// We received an empty string and have no mapping for it, and the property isn't nullable
_unknownValuesWarned.Add(null!);
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
}
@ -150,9 +149,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return (T)_undefinedEnumValue;
}
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
{
isEmptyStringOrNull = false;
isEmptyString = false;
var enumType = typeof(T);
if (_mappingToEnum == null)
CreateMapping();
@ -168,16 +167,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
};
if (stringValue is null)
{
isEmptyStringOrNull = true;
return null;
}
if (!GetValue(enumType, stringValue, out var result))
{
if (string.IsNullOrWhiteSpace(stringValue))
{
isEmptyStringOrNull = true;
isEmptyString = true;
}
else
{
@ -309,12 +305,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif
}
internal static void Reset()
{
_undefinedEnumValue = null;
_unknownValuesWarned = new ConcurrentBag<string>();
}
/// <summary>
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
/// </summary>

View File

@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>11.0.3</PackageVersion>
<AssemblyVersion>11.0.3</AssemblyVersion>
<FileVersion>11.0.3</FileVersion>
<PackageVersion>11.0.1</PackageVersion>
<AssemblyVersion>11.0.1</AssemblyVersion>
<FileVersion>11.0.1</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;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>

View File

@ -28,12 +28,6 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// Whether client is disposed
/// </summary>
bool Disposed { get; }
/// <summary>
/// Update specific options
/// </summary>
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
void SetOptions(UpdateOptions options);
}
/// <inheritdoc />

View File

@ -60,12 +60,6 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary>
/// <returns></returns>
Task UnsubscribeAllAsync();
/// <summary>
/// Update specific options
/// </summary>
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
void SetOptions(UpdateOptions options);
}
/// <inheritdoc />

View File

@ -154,9 +154,6 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns>
public CallResult AsDataless()
{
if (Error != null )
return new CallResult(Error);
return SuccessResult;
}

View File

@ -6,27 +6,19 @@ namespace CryptoExchange.Net.Objects.Options
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions
public class UpdateOptions<TApiCredentials> where TApiCredentials : ApiCredentials
{
/// <summary>
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
/// </summary>
public ApiProxy? Proxy { get; set; }
/// <summary>
/// Api credentials
/// </summary>
public TApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Request timeout
/// </summary>
public TimeSpan? RequestTimeout { get; set; }
}
/// <summary>
/// Options to update
/// </summary>
public class UpdateOptions<TApiCredentials>: UpdateOptions
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Api credentials
/// </summary>
public TApiCredentials? ApiCredentials { get; set; }
}
}

View File

@ -3,7 +3,6 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using System;
using System.Collections.Generic;
using System.Threading;
namespace CryptoExchange.Net.RateLimiting.Guards
{
@ -37,7 +36,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
private readonly double? _decayRate;
private readonly int? _connectionWeight;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
private readonly SemaphoreSlim? _sharedGuardSemaphore;
/// <inheritdoc />
public string Name => "RateLimitGuard";
@ -54,11 +52,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// </summary>
public TimeSpan TimeSpan { get; }
/// <summary>
/// Whether this guard is shared between multiple gates
/// </summary>
public bool SharedGuard { get; }
/// <summary>
/// ctor
/// </summary>
@ -69,9 +62,8 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
{
}
@ -85,27 +77,22 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="windowType">Type of rate limit window</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
{
_filters = filters;
_trackers = new Dictionary<string, IWindowTracker>();
_windowType = windowType;
Limit = limit;
TimeSpan = timeSpan;
SharedGuard = shared;
_keySelector = keySelector;
_decayRate = decayPerTimeSpan;
_connectionWeight = connectionWeight;
if (SharedGuard)
_sharedGuardSemaphore = new SemaphoreSlim(1, 1);
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
{
foreach (var filter in _filters)
foreach(var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
return LimitCheck.NotApplicable;
@ -114,11 +101,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
if (type == RateLimitItemType.Connection)
requestWeight = _connectionWeight ?? requestWeight;
if (SharedGuard)
_sharedGuardSemaphore!.Wait();
try
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
{
@ -126,19 +108,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
_trackers.Add(key, tracker);
}
var delay = tracker.GetWaitTime(requestWeight);
if (delay == default)
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
}
finally
{
if (SharedGuard)
_sharedGuardSemaphore!.Release();
}
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
@ -152,23 +127,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
if (type == RateLimitItemType.Connection)
requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey) + keySuffix;
var tracker = _trackers[key];
if (SharedGuard)
_sharedGuardSemaphore!.Wait();
try
{
tracker.ApplyWeight(requestWeight);
}
finally
{
if (SharedGuard)
_sharedGuardSemaphore!.Release();
}
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
}

View File

@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Testing
if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message);
if (message.Contains("Received null or empty enum value"))
if (message.Contains("Received null enum value"))
throw new Exception("Enum null error: " + message);
}
@ -25,7 +25,7 @@ namespace CryptoExchange.Net.Testing
if (message.Contains("Cannot map"))
throw new Exception("Enum value error: " + message);
if (message.Contains("Received null or empty enum value"))
if (message.Contains("Received null enum value"))
throw new Exception("Enum null error: " + message);
}
}

View File

@ -1,126 +0,0 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
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 SharedRestRequestValidator<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;
/// <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>
public SharedRestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null)
{
_client = client;
_folder = folder;
_baseAddress = baseAddress;
_nestedPropertyForCompare = nestedPropertyForCompare;
_isAuthenticated = isAuthenticated;
}
/// <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="endpointOptions">Request options</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public Task ValidateAsync<TResponse>(
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
string name,
EndpointOptions endpointOptions,
params Func<TResponse, bool>[] validation)
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, endpointOptions, validation);
/// <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="endpointOptions">Request options</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task ValidateAsync<TResponse, TActualResponse>(
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
string name,
EndpointOptions endpointOptions,
params Func<TResponse, bool>[] validation) 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 (endpointOptions.NeedsAuthentication != 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]}");
var index = 0;
foreach(var validate in validation)
{
if (!validate(result.Data!))
throw new Exception(name + $" response validation #{index} failed");
index++;
}
Trace.Listeners.Remove(listener);
}
}
}

View File

@ -5,32 +5,32 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.11.0" />
<PackageReference Include="Bitfinex.Net" Version="10.10.1" />
<PackageReference Include="BitMart.Net" Version="3.9.1" />
<PackageReference Include="BloFin.Net" Version="2.10.1" />
<PackageReference Include="Bybit.Net" Version="6.10.0" />
<PackageReference Include="CoinEx.Net" Version="10.9.1" />
<PackageReference Include="CoinW.Net" Version="2.9.1" />
<PackageReference Include="CryptoCom.Net" Version="3.9.1" />
<PackageReference Include="DeepCoin.Net" Version="3.9.1" />
<PackageReference Include="GateIo.Net" Version="3.10.1" />
<PackageReference Include="HyperLiquid.Net" Version="4.0.1" />
<PackageReference Include="JK.BingX.Net" Version="3.9.1" />
<PackageReference Include="JK.Bitget.Net" Version="3.9.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.9.0" />
<PackageReference Include="JK.OKX.Net" Version="4.10.1" />
<PackageReference Include="Jkorf.Aster.Net" Version="3.0.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="3.9.1" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.9.1" />
<PackageReference Include="JKorf.HTX.Net" Version="8.9.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="2.9.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.9.0" />
<PackageReference Include="Kucoin.Net" Version="8.10.1" />
<PackageReference Include="Binance.Net" Version="12.5.0" />
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
<PackageReference Include="BitMart.Net" Version="3.5.0" />
<PackageReference Include="BloFin.Net" Version="2.5.0" />
<PackageReference Include="Bybit.Net" Version="6.5.0" />
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
<PackageReference Include="CoinW.Net" Version="2.5.0" />
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
<PackageReference Include="DeepCoin.Net" Version="3.5.0" />
<PackageReference Include="GateIo.Net" Version="3.5.0" />
<PackageReference Include="HyperLiquid.Net" Version="3.7.0" />
<PackageReference Include="JK.BingX.Net" Version="3.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="2.5.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="3.5.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="2.5.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="3.9.1" />
<PackageReference Include="WhiteBit.Net" Version="3.9.1" />
<PackageReference Include="XT.Net" Version="3.9.1" />
<PackageReference Include="Toobit.Net" Version="3.5.0" />
<PackageReference Include="WhiteBit.Net" Version="3.5.0" />
<PackageReference Include="XT.Net" Version="3.5.0" />
</ItemGroup>
</Project>

View File

@ -21,7 +21,6 @@
@using HTX.Net.Interfaces
@using HyperLiquid.Net.Interfaces
@using Kraken.Net.Interfaces
@using Kucoin.Net
@using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces
@ -84,7 +83,7 @@
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
KucoinRestClient.SetDefaultOptions(options =>
{
options.ApiCredentials = new KucoinCredentials("KEY", "SECRET", "PASSPHRASE");
options.ApiCredentials = new ApiCredentials("KEY", "SECRET", "PASSPHRASE");
});
_books = new Dictionary<string, ISymbolOrderBook>

View File

@ -1,4 +1,3 @@
using Binance.Net;
using CryptoExchange.Net.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -31,7 +30,7 @@ namespace BlazorClient
// Register the clients, options can be provided in the callback parameter
services.AddBinance(restOptions =>
{
restOptions.ApiCredentials = new BinanceCredentials("KEY", "SECRET");
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
});
services.AddAster();

View File

@ -6,20 +6,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.11.0" />
<PackageReference Include="Bitfinex.Net" Version="10.10.1" />
<PackageReference Include="BitMart.Net" Version="3.9.1" />
<PackageReference Include="Bybit.Net" Version="6.10.0" />
<PackageReference Include="CoinEx.Net" Version="10.9.1" />
<PackageReference Include="CryptoCom.Net" Version="3.9.1" />
<PackageReference Include="GateIo.Net" Version="3.10.1" />
<PackageReference Include="JK.Bitget.Net" Version="3.9.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.9.0" />
<PackageReference Include="JK.OKX.Net" Version="4.10.1" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.9.1" />
<PackageReference Include="JKorf.HTX.Net" Version="8.9.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.9.0" />
<PackageReference Include="Kucoin.Net" Version="8.10.1" />
<PackageReference Include="Binance.Net" Version="12.5.0" />
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
<PackageReference Include="BitMart.Net" Version="3.5.0" />
<PackageReference Include="Bybit.Net" Version="6.5.0" />
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
<PackageReference Include="GateIo.Net" Version="3.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
</ItemGroup>
</Project>

View File

@ -3,9 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Binance.Net;
using Binance.Net.Clients;
using Bybit.Net;
using Bybit.Net.Clients;
using ConsoleClient.Exchanges;
using CryptoExchange.Net.Authentication;
@ -17,19 +15,19 @@ namespace ConsoleClient
{
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
{
{ "Binance", new Exchanges.BinanceExchange() },
{ "Bybit", new Exchanges.BybitExchange() }
{ "Binance", new BinanceExchange() },
{ "Bybit", new BybitExchange() }
};
static async Task Main(string[] args)
{
BinanceRestClient.SetDefaultOptions(options =>
{
options.ApiCredentials = new BinanceCredentials("APIKEY", "APISECRET");
options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
});
BybitRestClient.SetDefaultOptions(options =>
{
options.ApiCredentials = new BybitCredentials("APIKEY", "APISECRET");
options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
});
while (true)

View File

@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.11.0" />
<PackageReference Include="BitMart.Net" Version="3.9.1" />
<PackageReference Include="JK.OKX.Net" Version="4.10.1" />
<PackageReference Include="Binance.Net" Version="12.5.0" />
<PackageReference Include="BitMart.Net" Version="3.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
</ItemGroup>
</Project>

View File

@ -68,12 +68,6 @@ 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 11.0.3 - 30 Mar 2026
* Updated Enum converter to only warn once per type for null/empty value for non-nullable enum property
* Version 11.0.2 - 26 Mar 2026
* Updated SetOptions logic to allow calling on client without credentials
* Version 11.0.1 - 24 Mar 2026
* Fixed CreateParamString method for arrays of value types