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

Compare commits

..

24 Commits

Author SHA1 Message Date
Jkorf
93034e8af8 Fixed concurrency issue when using rate limit guard for multiple gates 2026-04-07 09:57:47 +02:00
Jkorf
cdd0bd83ab Added SharedRestRequestValidator for testing Shared interface implementations 2026-04-03 13:08:59 +02:00
JKorf
61d371682c Fixed CallResult returning success result in AsDataless even if Error is set 2026-03-30 19:56:58 +02:00
Jkorf
8aae769e54 Updated to version 11.0.3 2026-03-30 11:46:03 +02:00
Jkorf
1b4f6926df Fixed test concurrency issue 2026-03-30 11:42:26 +02:00
Jkorf
acca3468f3 Updated Enum converter to only warn once per type for null/empty value for non-nullable enum property 2026-03-30 11:34:02 +02:00
Jkorf
d6b680d42e Updated to version 11.0.2 2026-03-26 13:01:38 +01:00
JKorf
24123261e5 Fixed example 2026-03-25 21:40:40 +01:00
JKorf
aab30e05f0 Updated SetOptions logic to allow calling on client without credentials 2026-03-25 21:06:54 +01:00
Jkorf
54b15b75d3 Updated examples 2026-03-24 17:25:58 +01:00
Jkorf
d715c7df59 Updated to version 11.0.1 2026-03-24 14:38:39 +01:00
Jkorf
e1d33f252f Fixed CreateParamString method for arrays of value types 2026-03-24 14:28:46 +01:00
Jkorf
b2ba9e251f Updated to version 11.0.0 2026-03-23 14:37:34 +01:00
Jan Korf
2f9f97f4e8
Authentication update (#275)
Updated API credential logic, exchange implementation are expected to provide their own credentials implementation with ApiCredentials as base class
Removed ApiCredentials implementation used by most exchanges
Removed ApiCredentialsType Enum
Added CredentialSet base class and implementations for defining different API credentials
Added optional type param to AuthenticationProvider for the specific API credential type to improve type safety
Moved AuthenticationProvider/ApiCredentials from BaseApiClient to RestApiClient/SocketApiClient base classes
Added optional type params to RestApiClient/SocketApiClient base class to specify the AuthenticationProvider type and credentials type to improve type safety
Moved SetOptions/SetApiCredentials from BaseApiClient to RestApiClient/SocketApiClient
Extracted LibraryOptions<TRestOptions, TSocketOptions, TEnvironment> without TApiCredentials for libraries without API credentials
Removed ApiCredentials from ApiOptions, credentials can only be configured at library, rest or socket level
Added EnvironmentName to RestApiClient/SocketApiClient property
Added Unknown enum value to Shared interfaces SharedOrderStatus, SharedTransferStatus and SharedTriggerOrderStatus enums
Updated Enum converter to map value to an undefined Enum value instead of the first Enum value
Added support for checking for missing fields on RestIntegrationTest
Added BytesToHexString and HexToBytesString to ExchangeHelpers static class
Fixed bug where WebSocket connections are not reconnected when configuring Proxy with SetUpdates
Removed legacy CryptoBaseClient, CryptoRestClient and CryptoSocketClient
2026-03-23 14:28:00 +01:00
Jkorf
875696a73a added dex signing utils 2026-03-11 08:54:36 +01:00
Jkorf
90c61715d0 Added Bitstamp reference 2026-03-06 15:16:47 +01:00
Jkorf
12ca45050f Updated to version 10.8.0 2026-03-06 09:26:30 +01:00
Jkorf
eaf092a334 Added ForcePathEndWithSlash setting to RequestDefinition, added encoding parameter to SetContent on REST Request, added RequestBodyContentEncoding and OmitContentTypeHeaderWithoutContent config to RestApiClient 2026-03-06 09:25:04 +01:00
Jkorf
33c0fb26a7 Updated to version 10.7.2 2026-03-02 10:53:37 +01:00
Jkorf
dabbb5c868 Added small overlap in UserDataTracker polling logic to account for API endpoints not immediately having the data available 2026-03-02 10:47:25 +01:00
Jkorf
204bda8622 Updated to version 10.7.1 2026-02-25 11:00:43 +01:00
Jkorf
78e3523a4f Updated SocketConnection internal locking to fix potential deadlock 2026-02-25 09:26:12 +01:00
Jkorf
89a73747b0 Fix for test socket not working correctly with read/write socket connection lock 2026-02-24 13:15:08 +01:00
Jkorf
02b70398b3 Updated lock call SocketConnection to outside the try 2026-02-24 13:14:48 +01:00
73 changed files with 3109 additions and 1189 deletions

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
using NUnit.Framework; using NUnit.Framework;
@ -29,7 +30,13 @@ namespace CryptoExchange.Net.UnitTests
// act // act
// assert // assert
Assert.Throws(typeof(ArgumentException), Assert.Throws(typeof(ArgumentException),
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) }); () => {
var opts = new RestExchangeOptions<TestEnvironment, HMACCredential>()
{
ApiCredentials = new HMACCredential(key, secret)
};
opts.ApiCredentials.Validate();
});
} }
[Test] [Test]
@ -38,7 +45,7 @@ namespace CryptoExchange.Net.UnitTests
// arrange, act // arrange, act
var options = new TestClientOptions var options = new TestClientOptions
{ {
ApiCredentials = new ApiCredentials("123", "456"), ApiCredentials = new HMACCredential("123", "456"),
ReceiveWindow = TimeSpan.FromSeconds(10) ReceiveWindow = TimeSpan.FromSeconds(10)
}; };
@ -49,83 +56,65 @@ namespace CryptoExchange.Net.UnitTests
} }
[Test] [Test]
public void TestApiOptionsAreSet() public void TestSetOptionsRest()
{ {
// arrange, act
var options = new TestClientOptions();
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
// assert
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
}
[Test]
public void TestClientUsesCorrectOptions()
{
var client = new TestRestClient(options => {
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
options.ApiCredentials = new ApiCredentials("333", "444");
});
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "111");
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "333");
Assert.That(authProvider2.GetSecret() == "444");
}
[Test]
public void TestClientUsesCorrectOptionsWithDefault()
{
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient(); var client = new TestRestClient();
client.SetOptions(new UpdateOptions
{
RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
});
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
Assert.That(authProvider1.GetKey() == "111"); Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(authProvider1.GetSecret() == "222"); Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
} }
[Test] [Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault() public void TestSetOptionsRestWithCredentials()
{ {
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456"); var client = new TestRestClient();
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222"); client.SetOptions(new UpdateOptions<HMACCredential>
var client = new TestRestClient(options =>
{ {
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444"); ApiCredentials = new HMACCredential("123", "456"),
options.Environment = new TestEnvironment("Test", "https://test.test"); RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
}); });
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; Assert.That(client.Api1.ApiCredentials, Is.Not.Null);
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; Assert.That(client.Api1.ApiCredentials.Key, Is.EqualTo("123"));
Assert.That(authProvider1.GetKey() == "333"); Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(authProvider1.GetSecret() == "444"); Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
Assert.That(authProvider2.GetKey() == "123"); Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(authProvider2.GetSecret() == "456"); Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(client.Api2.BaseAddress == "https://localhost:123"); }
// Cleanup static values [Test]
TestClientOptions.Default.ApiCredentials = null; public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
TestClientOptions.Default.Api1Options.ApiCredentials = null; {
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, ApiCredentials> public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
{ {
/// <summary> /// <summary>
/// Default options for the futures client /// Default options for the futures client

View File

@ -1,11 +1,15 @@
using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
@ -155,15 +159,22 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("three", TestEnum.Three)] [TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)] [TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)] [TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)] [TestCase("Four1", (TestEnum)(-9))]
[TestCase(null, TestEnum.One)] [TestCase(null, (TestEnum)(-9))]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected) public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
{ {
var val = value == null ? "null" : $"\"{value}\""; var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}"); var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected); Assert.That(output.Value == expected);
} }
[Test]
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
{
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
Assert.That((int)output == -99);
}
[TestCase("1", TestEnum.One)] [TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)] [TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)] [TestCase("3", TestEnum.Three)]
@ -178,6 +189,31 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(result == expected); 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("1", true)]
[TestCase("true", true)] [TestCase("true", true)]
[TestCase("yes", true)] [TestCase("yes", true)]
@ -425,6 +461,20 @@ namespace CryptoExchange.Net.UnitTests
Four Four
} }
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
public enum TestEnum2
{
[Map("-9")]
Minus9 = -9,
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonSerializable(typeof(Test))] [JsonSerializable(typeof(Test))]
[JsonSerializable(typeof(Test2))] [JsonSerializable(typeof(Test2))]
[JsonSerializable(typeof(Test3))] [JsonSerializable(typeof(Test3))]

View File

@ -42,11 +42,11 @@ namespace CryptoExchange.Net.UnitTests
} }
} }
public class TestSubClient : RestApiClient public class TestSubClient : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
{ {
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException(); protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions) public TestSubClient(RestExchangeOptions<TestEnvironment, HMACCredential> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{ {
} }
@ -58,15 +58,13 @@ namespace CryptoExchange.Net.UnitTests
/// <inheritdoc /> /// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions()); protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException(); protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
} }
public class TestAuthProvider : AuthenticationProvider public class TestAuthProvider : AuthenticationProvider<HMACCredential, HMACCredential>
{ {
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac]; public TestAuthProvider(HMACCredential credentials) : base(credentials, credentials)
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{ {
} }
@ -74,8 +72,8 @@ namespace CryptoExchange.Net.UnitTests
{ {
} }
public string GetKey() => _credentials.Key; public string GetKey() => Credential.Key;
public string GetSecret() => _credentials.Secret; public string GetSecret() => Credential.Secret;
} }
public class TestEnvironment : TradeEnvironment public class TestEnvironment : TradeEnvironment

View File

@ -23,7 +23,7 @@ using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
public class TestRestClient: BaseRestClient public class TestRestClient: BaseRestClient<TestEnvironment, HMACCredential>
{ {
public TestRestApi1Client Api1 { get; } public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; } public TestRestApi2Client Api2 { get; }
@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
Initialize(options.Value); Initialize(options.Value);
Api1 = new TestRestApi1Client(options.Value); Api1 = AddApiClient(new TestRestApi1Client(options.Value));
Api2 = new TestRestApi2Client(options.Value); Api2 = AddApiClient(new TestRestApi2Client(options.Value));
} }
public void SetResponse(string responseData, out IRequest requestObj) public void SetResponse(string responseData, out IRequest requestObj)
@ -56,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
var request = new Mock<IRequest>(); var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object)); request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); })); request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<Encoding>(), It.IsAny<string>())).Callback(new Action<string, Encoding, string>((content, encoding, type) => { request.Setup(r => r.Content).Returns(content); }));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val })); request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers); request.Setup(c => c.GetHeaders()).Returns(() => headers);
@ -130,7 +130,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
} }
} }
public class TestRestApi1Client : RestApiClient public class TestRestApi1Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
{ {
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler(); protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
@ -159,7 +159,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position; ParameterPositions[method] = position;
} }
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
=> new TestAuthProvider(credentials); => new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
@ -168,7 +168,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
} }
} }
public class TestRestApi2Client : RestApiClient public class TestRestApi2Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
{ {
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler(); protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
@ -187,7 +187,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct); return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
} }
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
=> new TestAuthProvider(credentials); => new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()

View File

@ -1,101 +1,19 @@
using System; namespace CryptoExchange.Net.Authentication
using System.IO;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Authentication
{ {
/// <summary> /// <summary>
/// Api credentials, used to sign requests accessing private endpoints /// Api credentials, used to sign requests accessing private endpoints
/// </summary> /// </summary>
public class ApiCredentials public abstract class ApiCredentials
{ {
/// <summary> /// <summary>
/// The api key / label to authenticate requests /// Validate the API credentials
/// </summary> /// </summary>
public string Key { get; set; } public abstract void Validate();
/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; set; }
/// <summary>
/// The api passphrase. Not needed on all exchanges
/// </summary>
public string? Pass { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="pass">The api pass for the key. Not always needed</param>
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialType;
Key = key;
Secret = secret;
Pass = pass;
}
/// <summary>
/// Create API credentials using an API key and secret generated by the server
/// </summary>
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
{
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in PEM format
/// </summary>
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
}
/// <summary>
/// Create API credentials using an API key and an RSA private key in XML format
/// </summary>
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
}
/// <summary>
/// Create API credentials using an API key and an Ed25519 private key
/// </summary>
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
{
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
}
/// <summary>
/// Load a key from a file
/// </summary>
public static string ReadFromFile(string path)
{
using var fileStream = File.OpenRead(path);
using var streamReader = new StreamReader(fileStream);
return streamReader.ReadToEnd();
}
/// <summary> /// <summary>
/// Copy the credentials /// Copy the credentials
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public virtual ApiCredentials Copy() public abstract ApiCredentials Copy();
{
return new ApiCredentials(Key, Secret, Pass, CredentialType);
}
} }
} }

View File

@ -1,25 +0,0 @@
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Credentials type
/// </summary>
public enum ApiCredentialsType
{
/// <summary>
/// Hmac keys credentials
/// </summary>
Hmac,
/// <summary>
/// Rsa keys credentials in xml format
/// </summary>
RsaXml,
/// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary>
RsaPem,
/// <summary>
/// Ed25519 keys credentials
/// </summary>
Ed25519
}
}

View File

@ -13,6 +13,7 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default; using CryptoExchange.Net.Sockets.Default;
using System.Net;
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication
{ {
@ -24,58 +25,9 @@ namespace CryptoExchange.Net.Authentication
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
/// <summary> /// <summary>
/// The supported credential types /// The public identifier for the provided credentials
/// </summary> /// </summary>
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; } public abstract string Key { get; }
/// <summary>
/// Provided credentials
/// </summary>
protected internal readonly ApiCredentials _credentials;
/// <summary>
/// Byte representation of the secret
/// </summary>
protected byte[] _sBytes;
#if NET8_0_OR_GREATER
/// <summary>
/// The Ed25519 private key
/// </summary>
protected Key? Ed25519Key;
#endif
/// <summary>
/// Get the API key of the current credentials
/// </summary>
public string ApiKey => _credentials.Key!;
/// <summary>
/// Get the Passphrase of the current credentials
/// </summary>
public string? Pass => _credentials.Pass;
/// <summary>
/// ctor
/// </summary>
/// <param name="credentials"></param>
protected AuthenticationProvider(ApiCredentials credentials)
{
if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed");
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
{
#if !NET8_0_OR_GREATER
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
#endif
}
_credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
}
/// <summary> /// <summary>
/// Authenticate a REST request /// Authenticate a REST request
@ -276,21 +228,15 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// HMACSHA256 sign the data and return the hash /// HMACSHA256 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> protected string SignHMACSHA256(HMACCredential credential, string data, SignOutputType? outputType = null)
/// <param name="outputType">String type</param> => SignHMACSHA256(credential,Encoding.UTF8.GetBytes(data), outputType);
/// <returns></returns>
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
/// <summary> /// <summary>
/// HMACSHA256 sign the data and return the hash /// HMACSHA256 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> protected string SignHMACSHA256(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA256(_sBytes); using var encryptor = new HMACSHA256(credential.GetSBytes());
var resultBytes = encryptor.ComputeHash(data); var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
@ -298,21 +244,15 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// HMACSHA384 sign the data and return the hash /// HMACSHA384 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> protected string SignHMACSHA384(HMACCredential credential, string data, SignOutputType? outputType = null)
/// <param name="outputType">String type</param> => SignHMACSHA384(credential, Encoding.UTF8.GetBytes(data), outputType);
/// <returns></returns>
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
/// <summary> /// <summary>
/// HMACSHA384 sign the data and return the hash /// HMACSHA384 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> protected string SignHMACSHA384(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA384(_sBytes); using var encryptor = new HMACSHA384(credential.GetSBytes());
var resultBytes = encryptor.ComputeHash(data); var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
@ -320,21 +260,15 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// HMACSHA512 sign the data and return the hash /// HMACSHA512 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> protected string SignHMACSHA512(HMACCredential credential, string data, SignOutputType? outputType = null)
/// <param name="outputType">String type</param> => SignHMACSHA512(credential, Encoding.UTF8.GetBytes(data), outputType);
/// <returns></returns>
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
/// <summary> /// <summary>
/// HMACSHA512 sign the data and return the hash /// HMACSHA512 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> protected string SignHMACSHA512(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA512(_sBytes); using var encryptor = new HMACSHA512(credential.GetSBytes());
var resultBytes = encryptor.ComputeHash(data); var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
@ -342,27 +276,21 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// SHA256 sign the data /// SHA256 sign the data
/// </summary> /// </summary>
/// <param name="data"></param> protected string SignRSASHA256(RSACredential credential, byte[] data, SignOutputType? outputType = null)
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
{ {
using var rsa = CreateRSA(); var rsa = credential.GetSigner();
using var sha256 = SHA256.Create(); using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(data); var hash = sha256.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
/// <summary> /// <summary>
/// SHA384 sign the data /// SHA384 sign the data
/// </summary> /// </summary>
/// <param name="data"></param> protected string SignRSASHA384(RSACredential credential, byte[] data, SignOutputType? outputType = null)
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
{ {
using var rsa = CreateRSA(); var rsa = credential.GetSigner();
using var sha384 = SHA384.Create(); using var sha384 = SHA384.Create();
var hash = sha384.ComputeHash(data); var hash = sha384.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1); var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
@ -372,79 +300,32 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// SHA512 sign the data /// SHA512 sign the data
/// </summary> /// </summary>
/// <param name="data"></param> protected string SignRSASHA512(RSACredential credential, byte[] data, SignOutputType? outputType = null)
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
{ {
using var rsa = CreateRSA(); var rsa = credential.GetSigner();
using var sha512 = SHA512.Create(); using var sha512 = SHA512.Create();
var hash = sha512.ComputeHash(data); var hash = sha512.ComputeHash(data);
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(string data, SignOutputType? outputType = null)
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
{
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
if (Ed25519Key == null) /// <summary>
{ /// Ed25519 sign the data
var key = _credentials.Secret! /// </summary>
.Replace("\n", "") public string SignEd25519(Ed25519Credential credential, string data, SignOutputType? outputType = null)
.Replace("-----BEGIN PRIVATE KEY-----", "") => SignEd25519(credential, Encoding.ASCII.GetBytes(data), outputType);
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
}
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data); /// <summary>
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); /// Ed25519 sign the data
#else /// </summary>
throw new InvalidOperationException(); public string SignEd25519(Ed25519Credential credential, byte[] data, SignOutputType? outputType = null)
#endif
}
private RSA CreateRSA()
{ {
var rsa = RSA.Create(); var signKey = credential.GetSigningKey();
if (_credentials.CredentialType == ApiCredentialsType.RsaPem) var resultBytes = SignatureAlgorithm.Ed25519.Sign(signKey, data);
{ return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
// Read from pem private key
var key = _credentials.Secret!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
#else
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
#endif
}
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
{
// Read from xml private key format
rsa.FromXmlString(_credentials.Secret!);
}
else
{
throw new Exception("Invalid credentials type");
}
return rsa;
} }
#endif
/// <summary> /// <summary>
/// Convert byte array to hex string /// Convert byte array to hex string
@ -571,17 +452,179 @@ namespace CryptoExchange.Net.Authentication
} }
/// <inheritdoc /> /// <inheritdoc />
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider
where TApiCredentials: ApiCredentials
{ {
/// <inheritdoc /> /// <summary>
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials; /// API credentials used for signing requests
/// </summary>
public TApiCredentials ApiCredentials { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="credentials"></param> protected AuthenticationProvider(TApiCredentials credentials)
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
{ {
credentials.Validate();
ApiCredentials = credentials;
} }
} }
/// <inheritdoc />
public abstract class AuthenticationProvider<TApiCredentials, TCredentialType> : AuthenticationProvider<TApiCredentials>
where TApiCredentials : ApiCredentials
where TCredentialType : CredentialSet
{
/// <summary>
/// The specific credential type used for signing requests.
/// </summary>
public TCredentialType Credential { get; }
/// <inheritdoc />
public override string Key => Credential.Key;
/// <summary>
/// ctor
/// </summary>
protected AuthenticationProvider(
TApiCredentials credentials,
TCredentialType? credential) : base(credentials)
{
if (credential == null)
throw new ArgumentException($"Missing \"{typeof(TCredentialType).Name}\" credentials on \"{credentials.GetType().Name}\"");
Credential = credential;
}
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA256 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not HMACCredential hmacCredential)
throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
return SignHMACSHA256(hmacCredential, data, outputType);
}
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA384 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not HMACCredential hmacCredential)
throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
return SignHMACSHA384(hmacCredential, data, outputType);
}
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
/// <summary>
/// HMACSHA512 sign the data and return the hash
/// </summary>
/// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param>
/// <returns></returns>
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not HMACCredential hmacCredential)
throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
return SignHMACSHA512(hmacCredential, data, outputType);
}
/// <summary>
/// SHA256 sign the data
/// </summary>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not RSACredential rsaCredential)
throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
return SignRSASHA256(rsaCredential, data, outputType);
}
/// <summary>
/// SHA384 sign the data
/// </summary>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not RSACredential rsaCredential)
throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
return SignRSASHA384(rsaCredential, data, outputType);
}
/// <summary>
/// SHA512 sign the data
/// </summary>
/// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns>
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not RSACredential rsaCredential)
throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
return SignRSASHA512(rsaCredential, data, outputType);
}
#if NET8_0_OR_GREATER
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(string data, SignOutputType? outputType = null)
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
{
if (Credential is not Ed25519Credential ed25519Credential)
throw new InvalidOperationException($"Invalid Ed25519 signing without Ed25519 credentials provided");
return SignEd25519(ed25519Credential, data, outputType);
}
#endif
}
} }

View File

@ -0,0 +1,569 @@
using System;
using System.Security.Cryptography;
using System.Text;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Base class for a set of credentials
/// </summary>
public abstract class CredentialSet : ApiCredentials
{
/// <summary>
/// The (public) key/identifier for this credential pair
/// </summary>
public string Key { get; set; }
/// <summary>
/// ctor
/// </summary>
public CredentialSet() { }
/// <summary>
/// ctor
/// </summary>
public CredentialSet(string key)
{
Key = key;
}
/// <summary>
/// Validate the API credential
/// </summary>
public override void Validate()
{
if (string.IsNullOrEmpty(Key))
throw new ArgumentException($"Key not set on {GetType().Name}", nameof(Key));
}
}
/// <summary>
/// Api key credentials
/// </summary>
public class ApiKeyCredential : CredentialSet
{
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key</param>
public ApiKeyCredential(string key) : base(key)
{
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ApiKeyCredential(Key);
}
/// <summary>
/// HMAC credentials
/// </summary>
public class HMACCredential : CredentialSet
{
private byte[]? _sBytes;
/// <summary>
/// API secret
/// </summary>
public string Secret { get; set; }
/// <summary>
/// ctor
/// </summary>
public HMACCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="secret">Api secret</param>
public HMACCredential(string key, string secret) : base(key)
{
Secret = secret;
}
/// <summary>
/// Get the secret value bytes
/// </summary>
/// <returns></returns>
public byte[] GetSBytes()
{
return _sBytes ??= Encoding.UTF8.GetBytes(Secret);
}
/// <inheritdoc />
public override ApiCredentials Copy() => new HMACCredential(Key, Secret);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Secret))
throw new ArgumentException($"Secret not set on {GetType().Name}", nameof(Secret));
}
}
/// <summary>
/// HMAC credentials
/// </summary>
public class HMACPassCredential : HMACCredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public HMACPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="secret">Api secret</param>
/// <param name="pass">Passphrase</param>
public HMACPassCredential(string key, string secret, string pass) : base(key, secret)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new HMACPassCredential(Key, Secret, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
/// <summary>
/// RSA credentials
/// </summary>
public abstract class RSACredential : CredentialSet
{
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public RSACredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSACredential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <summary>
/// Get RSA signer
/// </summary>
public abstract RSA GetSigner();
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// RSA credentials
/// </summary>
public abstract class RSAPassCredential : RSACredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public RSAPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public RSAPassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
PrivateKey = privateKey;
Pass = pass;
}
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
#if NETSTANDARD2_1_OR_GREATER || NET7_0_OR_GREATER
/// <summary>
/// RSA credentials in PEM/base64 format
/// </summary>
public class RSAPemCredential : RSACredential
{
/// <summary>
/// ctor
/// </summary>
public RSAPemCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSAPemCredential(string key, string privateKey) : base(key, privateKey)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAPemCredential(Key, PrivateKey);
}
/// <summary>
/// RSA PEM/Base64 credentials
/// </summary>
public class RSAPemPassCredential : RSAPassCredential
{
/// <summary>
/// ctor
/// </summary>
public RSAPemPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Api secret</param>
/// <param name="pass">Passphrase</param>
public RSAPemPassCredential(string key, string privateKey, string pass) : base(key, privateKey, pass)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAPemPassCredential(Key, PrivateKey, Pass);
}
#endif
/// <summary>
/// RSA credentials in XML format
/// </summary>
public class RSAXmlCredential : RSACredential
{
/// <summary>
/// ctor
/// </summary>
public RSAXmlCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSAXmlCredential(string key, string privateKey) : base(key, privateKey)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
rsa.FromXmlString(PrivateKey);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAXmlCredential(Key, PrivateKey);
}
/// <summary>
/// RSA XML credentials
/// </summary>
public class RSAXmlPassCredential : RSAPassCredential
{
/// <summary>
/// ctor
/// </summary>
public RSAXmlPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Api secret</param>
/// <param name="pass">Passphrase</param>
public RSAXmlPassCredential(string key, string privateKey, string pass) : base(key, privateKey, pass)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
rsa.FromXmlString(PrivateKey);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAXmlPassCredential(Key, PrivateKey, Pass);
}
#if NET8_0_OR_GREATER
/// <summary>
/// Credentials in Ed25519 format
/// </summary>
public class Ed25519Credential : CredentialSet
{
private NSec.Cryptography.Key? _signKey;
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public Ed25519Credential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public Ed25519Credential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <summary>
/// Get signing key
/// </summary>
public NSec.Cryptography.Key GetSigningKey()
{
if (_signKey != null)
return _signKey;
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
_signKey = NSec.Cryptography.Key.Import(NSec.Cryptography.SignatureAlgorithm.Ed25519, keyBytes, NSec.Cryptography.KeyBlobFormat.PkixPrivateKey);
return _signKey;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new Ed25519Credential(Key, PrivateKey);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// Ed25519 credentials
/// </summary>
public class Ed25519PassCredential : Ed25519Credential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public Ed25519PassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public Ed25519PassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new Ed25519PassCredential(Key, PrivateKey, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
#endif
/// <summary>
/// Credentials in ECDsa format
/// </summary>
public class ECDsaCredential : CredentialSet
{
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public ECDsaCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public ECDsaCredential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ECDsaCredential(Key, PrivateKey);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// ECDsa credentials
/// </summary>
public class ECDsaPassCredential : ECDsaCredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public ECDsaPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public ECDsaPassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ECDsaPassCredential(Key, PrivateKey, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
}

View File

@ -0,0 +1,147 @@
using System;
using System.Linq;
using System.Numerics;
using System.Text;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// ABI encoding
/// </summary>
public static class CeAbiEncoder
{
/// <summary>
/// ABI encode string as Sha3Keccack hashed byte value
/// </summary>
public static byte[] AbiValueEncodeString(string value)
{
var abiValueEncoded = CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(value));
return abiValueEncoded;
}
/// <summary>
/// ABI encode bool value as uint256 with 1 for true and 0 for false, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeBool(bool value)
=> AbiValueEncodeInt((byte)(value ? 1 : 0));
/// <summary>
/// ABI encode byte value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(byte value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode short value as int256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(short value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode int value as int256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(int value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode long value as int256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(long value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode ushort value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(ushort value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode uint value as uint256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(uint value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode ulong value as uint256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(ulong value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode big integer value as int256 or uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeBigInteger(bool signed, BigInteger value)
{
var result = new byte[32];
if (signed && value < 0)
{
// Pad with FF
for (int i = 0; i < result.Length; i++)
{
result[i] = 0xFF;
}
}
var t = value.ToByteArray();
if (t.Length == 33)
{
// Strip last byte
var strip1 = new byte[32];
Array.Copy(t, 0, strip1, 0, 32);
t = strip1;
}
if (BitConverter.IsLittleEndian)
t = t.AsEnumerable().Reverse().ToArray();
t.CopyTo(result, result.Length - t.Length);
return result;
}
/// <summary>
/// ABI encode address value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeAddress(string value)
{
var result = new byte[32];
var h = value.HexStringToBytes();
h.CopyTo(result, result.Length - h.Length);
return result;
}
/// <summary>
/// ABI encode hex string value as bytes32, as per ABI specification. The hex string is expected to be a 0x prefixed string, and the resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the hex string is shorter than 32 bytes. If the hex string is longer than 32 bytes, an exception will be thrown.
/// </summary>
/// <param name="length"></param>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeHexBytes(int length, string value)
=> AbiValueEncodeBytes(value.Length, value.HexStringToBytes());
/// <summary>
/// ABI encode byte array value as bytes32, as per ABI specification. The resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the byte array is shorter than 32 bytes. If the byte array is longer than 32 bytes, an exception will be thrown.
/// </summary>
/// <param name="length"></param>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static byte[] AbiValueEncodeBytes(int length, byte[] value)
{
if (length != 32)
throw new Exception("Only 32 bytes size supported");
if (value.Length == 32)
return value;
var result = new byte[32];
value.CopyTo(result, result.Length - value.Length);
return result;
}
}
}

View File

@ -0,0 +1,304 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// EIP712 Typed Data Encoder
/// </summary>
public static class CeEip712TypedDataEncoder
{
/// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary>
public static byte[] EncodeEip721(
string primaryType,
IEnumerable<(string Name, string Type, object Value)> domainFields,
IEnumerable<(string Name, string Type, object Value)> messageFields)
{
var data = new CeTypedDataRaw()
{
PrimaryType = primaryType,
DomainRawValues = domainFields.Select(x => new CeMemberValue
{
TypeName = x.Type,
Value = x.Value,
}).ToArray(),
Message = messageFields.Select(x => new CeMemberValue
{
TypeName = x.Type,
Value = x.Value,
}).ToArray(),
Types = new Dictionary<string, CeMemberDescription[]>
{
{
"EIP712Domain",
domainFields.Select(x => new CeMemberDescription
{
Name = x.Name,
Type = x.Type
}).ToArray()
},
{
primaryType,
messageFields.Select(x => new CeMemberDescription
{
Name = x.Name,
Type = x.Type
}).ToArray()
}
}
};
return EncodeTypedDataRaw(data);
}
/// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary>
public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData)
{
using var memoryStream = new MemoryStream();
using var writer = new BinaryWriter(memoryStream);
// Write 0x19 0x01 prefix
writer.Write((byte)0x19);
writer.Write((byte)0x01);
// Write domain
writer.Write(HashStruct(typedData.Types, "EIP712Domain", typedData.DomainRawValues));
// Write message
writer.Write(HashStruct(typedData.Types, typedData.PrimaryType, typedData.Message));
writer.Flush();
var result = memoryStream.ToArray();
return result;
}
private static byte[] HashStruct(IDictionary<string, CeMemberDescription[]> types, string primaryType, IEnumerable<CeMemberValue> message)
{
var memoryStream = new MemoryStream();
var writer = new BinaryWriter(memoryStream);
// Encode the type header
EncodeType(writer, types, primaryType);
// Encode the data
EncodeData(writer, types, message);
writer.Flush();
return CeSha3Keccack.CalculateHash(memoryStream.ToArray());
}
private static void EncodeData(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, IEnumerable<CeMemberValue> memberValues)
{
foreach (var memberValue in memberValues)
{
switch (memberValue.TypeName)
{
case var refType when IsReferenceType(refType):
writer.Write(HashStruct(types, memberValue.TypeName, (IEnumerable<CeMemberValue>)memberValue.Value));
break;
case "string":
writer.Write(CeAbiEncoder.AbiValueEncodeString((string)memberValue.Value));
break;
case "bool":
writer.Write(CeAbiEncoder.AbiValueEncodeBool((bool)memberValue.Value));
break;
case "address":
writer.Write(CeAbiEncoder.AbiValueEncodeAddress((string)memberValue.Value));
break;
default:
if (memberValue.TypeName.Contains("["))
{
var items = (IList)memberValue.Value;
var itemsMemberValues = new List<CeMemberValue>();
foreach (var item in items)
{
itemsMemberValues.Add(new CeMemberValue()
{
TypeName = memberValue.TypeName.Substring(0, memberValue.TypeName.LastIndexOf("[")),
Value = item
});
}
var memoryStream = new MemoryStream();
var writerItem = new BinaryWriter(memoryStream);
EncodeData(writerItem, types, itemsMemberValues);
writerItem.Flush();
writer.Write(CeSha3Keccack.CalculateHash(memoryStream.ToArray()));
}
else if (memberValue.TypeName.StartsWith("int") || memberValue.TypeName.StartsWith("uint"))
{
if (memberValue.Value is string v)
{
if (!BigInteger.TryParse(v, out BigInteger parsedOutput))
throw new Exception($"Failed to encode BigInteger string {v}");
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput));
}
else if (memberValue.Value is byte b)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(b));
}
else if (memberValue.Value is short s)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(s));
}
else if (memberValue.Value is int i)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(i));
}
else if (memberValue.Value is long l)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(l));
}
else if (memberValue.Value is ushort us)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(us));
}
else if (memberValue.Value is uint ui)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ui));
}
else if (memberValue.Value is ulong ul)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ul));
}
else
{
throw new Exception("Unknown number value");
}
}
else if (memberValue.TypeName.StartsWith("bytes"))
{
var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5));
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value));
}
break;
}
}
}
private static void EncodeType(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, string typeName)
{
var encodedTypes = EncodeTypes(types, typeName);
var encodedPrimaryType = encodedTypes.Single(x => x.Key == typeName);
var encodedReferenceTypes = encodedTypes.Where(x => x.Key != typeName).OrderBy(x => x.Key).Select(x => x.Value);
var fullyEncodedType = encodedPrimaryType.Value + string.Join(string.Empty, encodedReferenceTypes.ToArray());
writer.Write(CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(fullyEncodedType)));
}
/// <summary>
/// Create a list of type => type(parameters), for example:<br />
/// { IP712Domain, EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) }
/// </summary>
private static IList<KeyValuePair<string, string>> EncodeTypes(IDictionary<string, CeMemberDescription[]> types, string currentTypeName)
{
var currentTypeMembers = types[currentTypeName];
var currentTypeMembersEncoded = currentTypeMembers.Select(x => x.Type + " " + x.Name);
var result = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(currentTypeName, currentTypeName + "(" + string.Join(",", currentTypeMembersEncoded.ToArray()) + ")")
};
result.AddRange(currentTypeMembers.Select(x => x.Type.Contains("[") ? x.Type.Substring(0, x.Type.IndexOf("[")) : x.Type)
.Distinct()
.Where(IsReferenceType)
.SelectMany(x => EncodeTypes(types, x)));
return result;
}
internal static bool IsReferenceType(string typeName)
{
switch (typeName)
{
case var bytes when new Regex("bytes\\d+").IsMatch(bytes):
case var @uint when new Regex("uint\\d+").IsMatch(@uint):
case var @int when new Regex("int\\d+").IsMatch(@int):
case "bytes":
case "string":
case "bool":
case "address":
case var array when array.Contains("["):
return false;
default:
return true;
}
}
}
/// <summary>
/// Member description
/// </summary>
public class CeMemberDescription
{
/// <summary>
/// Name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Type
/// </summary>
public string Type { get; set; } = string.Empty;
}
/// <summary>
/// Member value
/// </summary>
public class CeMemberValue
{
/// <summary>
/// Type name
/// </summary>
public string TypeName { get; set; } = string.Empty;
/// <summary>
/// Value
/// </summary>
public object Value { get; set; } = string.Empty;
}
/// <summary>
/// Typed data raw, used for encoding EIP712 typed data with the provided primary type, domain fields and message fields.
/// </summary>
public class CeTypedDataRaw
{
/// <summary>
/// Type dictionary
/// </summary>
public IDictionary<string, CeMemberDescription[]> Types { get; set; } = new Dictionary<string, CeMemberDescription[]>();
/// <summary>
/// Primary type
/// </summary>
public string PrimaryType { get; set; } = string.Empty;
/// <summary>
/// Message values
/// </summary>
public CeMemberValue[] Message { get; set; } = [];
/// <summary>
/// Domain values
/// </summary>
public CeMemberValue[] DomainRawValues { get; set; } = [];
}
}

View File

@ -0,0 +1,385 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// Sha3 Keccack hashing, as per Ethereum specification, with 256 bit output
/// </summary>
public class CeSha3Keccack
{
/// <summary>
/// Calculate the Keccack256 hash of the provided data, as per Ethereum specification
/// </summary>
public static byte[] CalculateHash(byte[] data)
{
var digest = new CeKeccakDigest256();
var output = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, data.Length);
digest.DoFinal(output, 0);
return output;
}
}
internal class CeKeccakDigest256
{
private static readonly ulong[] _keccakRoundConstants = KeccakInitializeRoundConstants();
private static readonly int[] _keccakRhoOffsets = KeccakInitializeRhoOffsets();
private readonly int _rate;
private const int _stateLength = 1600 / 8;
private readonly ulong[] _state = new ulong[_stateLength / 8];
private readonly byte[] _dataQueue = new byte[1536 / 8];
private int _bitsInQueue;
private int _fixedOutputLength;
private bool _squeezing;
private int _bitsAvailableForSqueezing;
public CeKeccakDigest256()
{
_rate = 1600 - (256 << 1);
_bitsInQueue = 0;
_squeezing = false;
_bitsAvailableForSqueezing = 0;
_fixedOutputLength = 1600 - _rate >> 1;
}
internal void BlockUpdate(byte[] data, int length)
{
int bytesInQueue = _bitsInQueue >> 3;
int rateBytes = _rate >> 3;
int count = 0;
while (count < length)
{
if (bytesInQueue == 0 && count <= length - rateBytes)
{
do
{
KeccakAbsorb(data, count);
count += rateBytes;
} while (count <= length - rateBytes);
}
else
{
int partialBlock = Math.Min(rateBytes - bytesInQueue, length - count);
Array.Copy(data, count, _dataQueue, bytesInQueue, partialBlock);
bytesInQueue += partialBlock;
count += partialBlock;
if (bytesInQueue == rateBytes)
{
KeccakAbsorb(_dataQueue, 0);
bytesInQueue = 0;
}
}
}
_bitsInQueue = bytesInQueue << 3;
}
internal void DoFinal(byte[] output, int outOff)
{
Squeeze(output, outOff, _fixedOutputLength >> 3);
}
internal int GetDigestSize() => _fixedOutputLength >> 3;
protected void Squeeze(byte[] output, int off, int len)
{
if (!_squeezing)
PadAndSwitchToSqueezingPhase();
long outputLength = (long)len << 3;
long i = 0;
while (i < outputLength)
{
if (_bitsAvailableForSqueezing == 0)
{
KeccakPermutation();
KeccakExtract();
_bitsAvailableForSqueezing = _rate;
}
int partialBlock = (int)Math.Min(_bitsAvailableForSqueezing, outputLength - i);
Array.Copy(_dataQueue, _rate - _bitsAvailableForSqueezing >> 3, output, off + (int)(i >> 3),
partialBlock >> 3);
_bitsAvailableForSqueezing -= partialBlock;
i += partialBlock;
}
}
private static ulong[] KeccakInitializeRoundConstants()
{
ulong[] keccakRoundConstants = new ulong[24];
byte LFSRState = 0x01;
for (int i = 0; i < 24; i++)
{
keccakRoundConstants[i] = 0;
for (int j = 0; j < 7; j++)
{
int bitPosition = (1 << j) - 1;
// LFSR86540
bool loBit = (LFSRState & 0x01) != 0;
if (loBit)
keccakRoundConstants[i] ^= 1UL << bitPosition;
bool hiBit = (LFSRState & 0x80) != 0;
LFSRState <<= 1;
if (hiBit)
LFSRState ^= 0x71;
}
}
return keccakRoundConstants;
}
private static int[] KeccakInitializeRhoOffsets()
{
int[] keccakRhoOffsets = new int[25];
int x, y, t, newX, newY;
int rhoOffset = 0;
keccakRhoOffsets[0] = rhoOffset;
x = 1;
y = 0;
for (t = 1; t < 25; t++)
{
rhoOffset = rhoOffset + t & 63;
keccakRhoOffsets[x % 5 + 5 * (y % 5)] = rhoOffset;
newX = (0 * x + 1 * y) % 5;
newY = (2 * x + 3 * y) % 5;
x = newX;
y = newY;
}
return keccakRhoOffsets;
}
private void KeccakAbsorb(byte[] data, int off)
{
int count = _rate >> 6;
for (int i = 0; i < count; ++i)
{
_state[i] ^= Pack.LeToUInt64(data, off);
off += 8;
}
KeccakPermutation();
}
private void KeccakPermutation()
{
for (int i = 0; i < 24; i++)
{
Theta(_state);
Rho(_state);
Pi(_state);
Chi(_state);
Iota(_state, i);
}
}
private static ulong LeftRotate(ulong v, int r)
{
return v << r | v >> -r;
}
private static void Theta(ulong[] A)
{
ulong C0 = A[0 + 0] ^ A[0 + 5] ^ A[0 + 10] ^ A[0 + 15] ^ A[0 + 20];
ulong C1 = A[1 + 0] ^ A[1 + 5] ^ A[1 + 10] ^ A[1 + 15] ^ A[1 + 20];
ulong C2 = A[2 + 0] ^ A[2 + 5] ^ A[2 + 10] ^ A[2 + 15] ^ A[2 + 20];
ulong C3 = A[3 + 0] ^ A[3 + 5] ^ A[3 + 10] ^ A[3 + 15] ^ A[3 + 20];
ulong C4 = A[4 + 0] ^ A[4 + 5] ^ A[4 + 10] ^ A[4 + 15] ^ A[4 + 20];
ulong dX = LeftRotate(C1, 1) ^ C4;
A[0] ^= dX;
A[5] ^= dX;
A[10] ^= dX;
A[15] ^= dX;
A[20] ^= dX;
dX = LeftRotate(C2, 1) ^ C0;
A[1] ^= dX;
A[6] ^= dX;
A[11] ^= dX;
A[16] ^= dX;
A[21] ^= dX;
dX = LeftRotate(C3, 1) ^ C1;
A[2] ^= dX;
A[7] ^= dX;
A[12] ^= dX;
A[17] ^= dX;
A[22] ^= dX;
dX = LeftRotate(C4, 1) ^ C2;
A[3] ^= dX;
A[8] ^= dX;
A[13] ^= dX;
A[18] ^= dX;
A[23] ^= dX;
dX = LeftRotate(C0, 1) ^ C3;
A[4] ^= dX;
A[9] ^= dX;
A[14] ^= dX;
A[19] ^= dX;
A[24] ^= dX;
}
private static void Rho(ulong[] A)
{
// KeccakRhoOffsets[0] == 0
for (int x = 1; x < 25; x++)
{
A[x] = LeftRotate(A[x], _keccakRhoOffsets[x]);
}
}
private static void Pi(ulong[] A)
{
ulong a1 = A[1];
A[1] = A[6];
A[6] = A[9];
A[9] = A[22];
A[22] = A[14];
A[14] = A[20];
A[20] = A[2];
A[2] = A[12];
A[12] = A[13];
A[13] = A[19];
A[19] = A[23];
A[23] = A[15];
A[15] = A[4];
A[4] = A[24];
A[24] = A[21];
A[21] = A[8];
A[8] = A[16];
A[16] = A[5];
A[5] = A[3];
A[3] = A[18];
A[18] = A[17];
A[17] = A[11];
A[11] = A[7];
A[7] = A[10];
A[10] = a1;
}
private static void Chi(ulong[] A)
{
ulong chiC0, chiC1, chiC2, chiC3, chiC4;
for (int yBy5 = 0; yBy5 < 25; yBy5 += 5)
{
chiC0 = A[0 + yBy5] ^ ~A[(0 + 1) % 5 + yBy5] & A[(0 + 2) % 5 + yBy5];
chiC1 = A[1 + yBy5] ^ ~A[(1 + 1) % 5 + yBy5] & A[(1 + 2) % 5 + yBy5];
chiC2 = A[2 + yBy5] ^ ~A[(2 + 1) % 5 + yBy5] & A[(2 + 2) % 5 + yBy5];
chiC3 = A[3 + yBy5] ^ ~A[(3 + 1) % 5 + yBy5] & A[(3 + 2) % 5 + yBy5];
chiC4 = A[4 + yBy5] ^ ~A[(4 + 1) % 5 + yBy5] & A[(4 + 2) % 5 + yBy5];
A[0 + yBy5] = chiC0;
A[1 + yBy5] = chiC1;
A[2 + yBy5] = chiC2;
A[3 + yBy5] = chiC3;
A[4 + yBy5] = chiC4;
}
}
private static void Iota(ulong[] A, int indexRound)
{
A[0] ^= _keccakRoundConstants[indexRound];
}
private void PadAndSwitchToSqueezingPhase()
{
Debug.Assert(_bitsInQueue < _rate);
_dataQueue[_bitsInQueue >> 3] |= (byte)(1U << (_bitsInQueue & 7));
if (++_bitsInQueue == _rate)
{
KeccakAbsorb(_dataQueue, 0);
_bitsInQueue = 0;
}
{
int full = _bitsInQueue >> 6, partial = _bitsInQueue & 63;
int off = 0;
for (int i = 0; i < full; ++i)
{
_state[i] ^= Pack.LeToUInt64(_dataQueue, off);
off += 8;
}
if (partial > 0)
{
ulong mask = (1UL << partial) - 1UL;
_state[full] ^= Pack.LeToUInt64(_dataQueue, off) & mask;
}
_state[_rate - 1 >> 6] ^= 1UL << 63;
}
KeccakPermutation();
KeccakExtract();
_bitsAvailableForSqueezing = _rate;
_bitsInQueue = 0;
_squeezing = true;
}
private void KeccakExtract()
{
Pack.UInt64ToLe(_state, 0, _rate >> 6, _dataQueue, 0);
}
static class Pack
{
internal static ulong LeToUInt64(byte[] bs, int off)
{
uint lo = LeToUInt32(bs, off);
uint hi = LeToUInt32(bs, off + 4);
return (ulong)hi << 32 | lo;
}
internal static uint LeToUInt32(byte[] bs, int off)
{
return bs[off]
| (uint)bs[off + 1] << 8
| (uint)bs[off + 2] << 16
| (uint)bs[off + 3] << 24;
}
internal static void UInt64ToLe(ulong[] ns, int nsOff, int nsLen, byte[] bs, int bsOff)
{
for (int i = 0; i < nsLen; ++i)
{
UInt64ToLe(ns[nsOff + i], bs, bsOff);
bsOff += 8;
}
}
internal static void UInt64ToLe(ulong n, byte[] bs, int off)
{
UInt32ToLe((uint)n, bs, off);
UInt32ToLe((uint)(n >> 32), bs, off + 4);
}
internal static void UInt32ToLe(uint n, byte[] bs, int off)
{
bs[off] = (byte)n;
bs[off + 1] = (byte)(n >> 8);
bs[off + 2] = (byte)(n >> 16);
bs[off + 3] = (byte)(n >> 24);
}
}
}
}

View File

@ -1,5 +1,4 @@
using System; using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
@ -28,6 +27,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected bool _disposing; protected bool _disposing;
/// <summary>
/// Whether a proxy is configured
/// </summary>
protected bool _proxyConfigured;
/// <summary> /// <summary>
/// Name of the client /// Name of the client
/// </summary> /// </summary>
@ -43,11 +47,6 @@ namespace CryptoExchange.Net.Clients
} }
} }
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; }
/// <summary> /// <summary>
/// The environment this client communicates to /// The environment this client communicates to
/// </summary> /// </summary>
@ -58,12 +57,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public bool OutputOriginalData { get; } public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary> /// <summary>
/// Api options /// Api options
/// </summary> /// </summary>
@ -85,10 +78,14 @@ namespace CryptoExchange.Net.Clients
/// <param name="logger">Logger</param> /// <param name="logger">Logger</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param> /// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param> /// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param> /// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param> /// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions) protected BaseApiClient(
ILogger logger,
bool outputOriginalData,
string baseAddress,
ExchangeOptions clientOptions,
ApiOptions apiOptions)
{ {
_logger = logger; _logger = logger;
@ -96,19 +93,10 @@ namespace CryptoExchange.Net.Clients
ApiOptions = apiOptions; ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData; OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress; BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
if (ApiCredentials != null) _proxyConfigured = ClientOptions.Proxy != null;
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
} }
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc /> /// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
@ -122,25 +110,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message); public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>

View File

@ -92,16 +92,6 @@ namespace CryptoExchange.Net.Clients
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}"); _logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
} }
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary> /// <summary>
/// Register an API client /// Register an API client
/// </summary> /// </summary>

View File

@ -1,7 +1,11 @@
using System.Linq; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients
{ {
@ -10,6 +14,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient public abstract class BaseRestClient : BaseClient, IRestClient
{ {
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
/// <inheritdoc /> /// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade); public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
@ -24,5 +33,59 @@ namespace CryptoExchange.Net.Clients
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange"); LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
} }
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class BaseRestClient<TEnvironment, TApiCredentials> : BaseRestClient, IRestClient<TApiCredentials>
where TEnvironment : TradeEnvironment
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Api clients in this client
/// </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>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
} }
} }

View File

@ -1,5 +1,7 @@
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -19,6 +21,11 @@ namespace CryptoExchange.Net.Clients
{ {
#region fields #region fields
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
/// <summary> /// <summary>
/// If client is disposing /// If client is disposing
/// </summary> /// </summary>
@ -133,5 +140,58 @@ namespace CryptoExchange.Net.Clients
return result; return result;
} }
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class BaseSocketClient<TEnvironment, TApiCredentials> : BaseSocketClient, ISocketClient<TApiCredentials>
where TEnvironment : TradeEnvironment
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Api clients in this client
/// </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>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
}
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public virtual void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
} }
} }

View File

@ -1,67 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base crypto client
/// </summary>
public class CryptoBaseClient : IDisposable
{
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary>
/// Service provider
/// </summary>
protected readonly IServiceProvider? _serviceProvider;
/// <summary>
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_serviceCache = new Dictionary<Type, object>();
}
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
if (_serviceProvider == null)
{
// Create with default options
var createResult = createFunc();
_serviceCache.Add(typeof(T), createResult!);
return createResult;
}
var result = _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
_serviceCache.Add(type, result!);
return result;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_serviceCache.Clear();
}
}
}

View File

@ -1,24 +0,0 @@
using CryptoExchange.Net.Interfaces.Clients;
using System;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoRestClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
}

View File

@ -1,24 +0,0 @@
using CryptoExchange.Net.Interfaces.Clients;
using System;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoSocketClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
}

View File

@ -1,3 +1,4 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Caching; using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
@ -77,6 +78,16 @@ namespace CryptoExchange.Net.Clients
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody }, { new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
}; };
/// <summary>
/// Encoding/charset for the ContentType header
/// </summary>
protected Encoding? RequestBodyContentEncoding { get; set; } = Encoding.UTF8;
/// <summary>
/// Whether to omit the ContentType header if there is no content
/// </summary>
protected bool OmitContentTypeHeaderWithoutContent { get; set; } = false;
/// <inheritdoc /> /// <inheritdoc />
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
@ -93,6 +104,16 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected abstract IRestMessageHandler MessageHandler { get; } protected abstract IRestMessageHandler MessageHandler { get; }
/// <summary>
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
/// </summary>
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
/// <summary>
/// Configured environment name
/// </summary>
public abstract string EnvironmentName { get; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@ -101,10 +122,13 @@ namespace CryptoExchange.Net.Clients
/// <param name="baseAddress">Base address for this API client</param> /// <param name="baseAddress">Base address for this API client</param>
/// <param name="options">The base client options</param> /// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param> /// <param name="apiOptions">The Api client options</param>
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) public RestApiClient(ILogger logger,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions options,
RestApiOptions apiOptions)
: base(logger, : base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData, apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress, baseAddress,
options, options,
apiOptions) apiOptions)
@ -204,7 +228,7 @@ namespace CryptoExchange.Net.Clients
string? rateLimitKeySuffix = null) string? rateLimitKeySuffix = null)
{ {
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
if (definition.Authenticated && AuthenticationProvider == null) if (definition.Authenticated && GetAuthenticationProvider() == null)
{ {
_logger.RestApiNoApiCredentials(requestId, definition.Path); _logger.RestApiNoApiCredentials(requestId, definition.Path);
return new WebCallResult<T>(new NoApiCredentialsError()); return new WebCallResult<T>(new NoApiCredentialsError());
@ -309,7 +333,17 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); var limitResult = await definition.RateLimitGate.ProcessAsync(
_logger,
requestId,
RateLimitItemType.Request,
definition,
host,
GetAuthenticationProvider()?.Key,
requestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix,
cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return limitResult.Error!; return limitResult.Error!;
} }
@ -324,7 +358,18 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var singleRequestWeight = weightSingleLimiter ?? 1; var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
_logger,
requestId,
definition.LimitGuard,
RateLimitItemType.Request,
definition,
host,
GetAuthenticationProvider()?.Key,
singleRequestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix,
cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return limitResult.Error!; return limitResult.Error!;
} }
@ -363,7 +408,7 @@ namespace CryptoExchange.Net.Clients
try try
{ {
AuthenticationProvider?.ProcessRequest(this, requestConfiguration); GetAuthenticationProvider()?.ProcessRequest(this, requestConfiguration);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -374,7 +419,11 @@ namespace CryptoExchange.Net.Clients
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?")) if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
queryString = $"?{queryString}"; queryString = $"?{queryString}";
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); var path = baseAddress.AppendPath(definition.Path);
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
path += "/";
var uri = new Uri(path + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId); var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
request.Accept = MessageHandler.AcceptHeader; request.Accept = MessageHandler.AcceptHeader;
@ -398,14 +447,14 @@ namespace CryptoExchange.Net.Clients
var bodyContent = requestConfiguration.GetBodyContent(); var bodyContent = requestConfiguration.GetBodyContent();
if (bodyContent != null) if (bodyContent != null)
{ {
request.SetContent(bodyContent, contentType); request.SetContent(bodyContent, RequestBodyContentEncoding, contentType);
} }
else else
{ {
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0) if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType); WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else else if (OmitContentTypeHeaderWithoutContent != true)
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
} }
} }
@ -646,13 +695,13 @@ namespace CryptoExchange.Net.Clients
stringData = stringSerializer.Serialize(value); stringData = stringSerializer.Serialize(value);
else else
stringData = stringSerializer.Serialize(parameters); stringData = stringSerializer.Serialize(parameters);
request.SetContent(stringData, contentType); request.SetContent(stringData, RequestBodyContentEncoding, contentType);
} }
else if (contentType == Constants.FormContentHeader) else if (contentType == Constants.FormContentHeader)
{ {
// Write the parameters as form data in the body // Write the parameters as form data in the body
var stringData = parameters.ToFormData(); var stringData = parameters.ToFormData();
request.SetContent(stringData, contentType); request.SetContent(stringData, RequestBodyContentEncoding, contentType);
} }
} }
@ -675,14 +724,6 @@ namespace CryptoExchange.Net.Clients
/// <returns>Server time</returns> /// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException(); protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition) private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
{ {
if (!definition.Authenticated) if (!definition.Authenticated)
@ -778,5 +819,170 @@ namespace CryptoExchange.Net.Clients
&& definition.Method == HttpMethod.Get && definition.Method == HttpMethod.Get
&& !definition.PreventCaching; && !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 />
public abstract class RestApiClient<TEnvironment> : RestApiClient, IRestApiClient
where TEnvironment : TradeEnvironment
{
/// <inheritdoc />
public new RestExchangeOptions<TEnvironment> ClientOptions => (RestExchangeOptions<TEnvironment>)base.ClientOptions;
/// <inheritdoc />
public override string EnvironmentName => ClientOptions.Environment.Name;
/// <summary>
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions options,
RestApiOptions apiOptions) : base(
logger,
httpClient,
baseAddress,
options,
apiOptions)
{
}
}
/// <inheritdoc />
public abstract class RestApiClient<TEnvironment, TApiCredentials> : RestApiClient<TEnvironment>, IRestApiClient<TApiCredentials>
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
/// <inheritdoc />
public TApiCredentials? ApiCredentials { get; set; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions<TEnvironment, TApiCredentials> options,
RestApiOptions apiOptions) : base(
logger,
httpClient,
baseAddress,
options,
apiOptions)
{
ApiCredentials = options.ApiCredentials;
}
/// <inheritdoc />
public virtual void SetApiCredentials(TApiCredentials credentials)
{
ApiCredentials = (TApiCredentials)credentials.Copy();
}
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
base.SetOptions(options);
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
}
}
/// <inheritdoc />
public abstract class RestApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : RestApiClient<TEnvironment, TApiCredentials>
where TApiCredentials : ApiCredentials
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
where TEnvironment : TradeEnvironment
{
private bool _authProviderInitialized = false;
private TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public TAuthenticationProvider? AuthenticationProvider
{
get
{
if (!_authProviderInitialized)
{
if (ApiCredentials != null)
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
_authProviderInitialized = true;
}
return _authenticationProvider;
}
internal set => _authenticationProvider = value;
}
/// <inheritdoc />
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
/// <summary>
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions<TEnvironment, TApiCredentials> options,
RestApiOptions apiOptions) : base(
logger,
httpClient,
baseAddress,
options,
apiOptions)
{
}
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
/// <inheritdoc />
public override void SetApiCredentials(TApiCredentials credentials)
{
base.SetApiCredentials(credentials);
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = credentials;
}
/// <inheritdoc />
public override void SetOptions(UpdateOptions<TApiCredentials> options)
{
base.SetOptions(options);
if (options.ApiCredentials != null)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = options.ApiCredentials;
}
}
} }
} }

View File

@ -1,3 +1,4 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters; using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
@ -19,6 +20,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -141,6 +143,16 @@ namespace CryptoExchange.Net.Clients
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1) /// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
/// </summary> /// </summary>
public bool EnforceSequenceNumbers { get; set; } public bool EnforceSequenceNumbers { get; set; }
/// <summary>
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
/// </summary>
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
/// <summary>
/// Configured environment name
/// </summary>
public abstract string EnvironmentName { get; }
#endregion #endregion
/// <summary> /// <summary>
@ -150,10 +162,13 @@ namespace CryptoExchange.Net.Clients
/// <param name="options">Client options</param> /// <param name="options">Client options</param>
/// <param name="baseAddress">Base address for this API client</param> /// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiOptions">The Api client options</param> /// <param name="apiOptions">The Api client options</param>
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) public SocketApiClient(
ILogger logger,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions)
: base(logger, : base(logger,
apiOptions.OutputOriginalData ?? options.OutputOriginalData, apiOptions.OutputOriginalData ?? options.OutputOriginalData,
apiOptions.ApiCredentials ?? options.ApiCredentials,
baseAddress, baseAddress,
options, options,
apiOptions) apiOptions)
@ -235,7 +250,7 @@ namespace CryptoExchange.Net.Clients
if (_disposing) if (_disposing)
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe")); return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null) if (subscription.Authenticated && GetAuthenticationProvider() == null)
{ {
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set"); _logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError()); return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
@ -515,7 +530,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket) public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
{ {
if (AuthenticationProvider == null) if (GetAuthenticationProvider() == null)
return new CallResult(new NoApiCredentialsError()); return new CallResult(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId); _logger.AttemptingToAuthenticate(socket.SocketId);
@ -546,7 +561,7 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection)); Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
/// <summary> /// <summary>
/// Adds a system subscription. Used for example to reply to ping requests /// Adds a system subscription. Used for example to reply to ping requests
@ -912,25 +927,6 @@ namespace CryptoExchange.Net.Clients
return CallResult.SuccessResult; return CallResult.SuccessResult;
} }
/// <inheritdoc />
public override void SetOptions<T>(UpdateOptions<T> options)
{
var previousProxyIsSet = ClientOptions.Proxy != null;
base.SetOptions(options);
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);
}
/// <summary> /// <summary>
/// Log the current state of connections and subscriptions /// Log the current state of connections and subscriptions
/// </summary> /// </summary>
@ -1039,5 +1035,174 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType); 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 />
public abstract class SocketApiClient<TEnvironment> : SocketApiClient, ISocketApiClient
where TEnvironment : TradeEnvironment
{
/// <inheritdoc />
public new SocketExchangeOptions<TEnvironment> ClientOptions => (SocketExchangeOptions<TEnvironment>)base.ClientOptions;
/// <inheritdoc />
public override string EnvironmentName => ClientOptions.Environment.Name;
/// <summary>
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
string baseAddress,
SocketExchangeOptions<TEnvironment> options,
SocketApiOptions apiOptions) : base(
logger,
baseAddress,
options,
apiOptions)
{
}
}
/// <inheritdoc />
public abstract class SocketApiClient<TEnvironment, TApiCredentials> : SocketApiClient<TEnvironment>, ISocketApiClient<TApiCredentials>
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
/// <inheritdoc />
public TApiCredentials? ApiCredentials { get; set; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
logger,
baseAddress,
options,
apiOptions)
{
ApiCredentials = options.ApiCredentials;
}
/// <inheritdoc />
public virtual void SetApiCredentials(TApiCredentials credentials)
{
ApiCredentials = (TApiCredentials)credentials.Copy();
}
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
base.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class SocketApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : SocketApiClient<TEnvironment, TApiCredentials>
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private bool _authProviderInitialized = false;
private TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public TAuthenticationProvider? AuthenticationProvider
{
get
{
if (!_authProviderInitialized)
{
if (ApiCredentials != null)
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
_authProviderInitialized = true;
}
return _authenticationProvider;
}
internal set => _authenticationProvider = value;
}
/// <inheritdoc />
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
/// <summary>
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
logger,
baseAddress,
options,
apiOptions)
{
}
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
/// <inheritdoc />
public override void SetApiCredentials(TApiCredentials credentials)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = credentials;
base.SetApiCredentials(credentials);
}
/// <inheritdoc />
public override void SetOptions(UpdateOptions<TApiCredentials> options)
{
if (options.ApiCredentials != null)
{
AuthenticationProvider = null;
_authProviderInitialized = false;
ApiCredentials = options.ApiCredentials;
}
base.SetOptions(options);
}
} }
} }

View File

@ -88,6 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif #endif
private NullableEnumConverter? _nullableEnumConverter = null; private NullableEnumConverter? _nullableEnumConverter = null;
private static T? _undefinedEnumValue;
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>(); private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
internal class NullableEnumConverter : JsonConverter<T?> internal class NullableEnumConverter : JsonConverter<T?>
@ -119,26 +120,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyStringOrNull);
if (t == null) if (t != null)
{
if (isEmptyString && !_unknownValuesWarned.Contains(null))
{
// We received an empty string and have no mapping for it, and the property isn't nullable
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");
}
return new T(); // return default value
}
else
{
return t.Value; return t.Value;
if (isEmptyStringOrNull && !_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");
} }
return GetUndefinedEnumValue();
} }
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString) private T GetUndefinedEnumValue()
{ {
isEmptyString = false; if (_undefinedEnumValue != null)
return _undefinedEnumValue.Value;
var type = typeof(T);
if (!Enum.IsDefined(type, -9))
_undefinedEnumValue = (T)Enum.ToObject(type, -9);
else if (!Enum.IsDefined(type, -99))
_undefinedEnumValue = (T)Enum.ToObject(type, -99);
else
_undefinedEnumValue = (T)Enum.ToObject(type, -999);
return (T)_undefinedEnumValue;
}
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
{
isEmptyStringOrNull = false;
var enumType = typeof(T); var enumType = typeof(T);
if (_mappingToEnum == null) if (_mappingToEnum == null)
CreateMapping(); CreateMapping();
@ -154,13 +168,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}; };
if (stringValue is null) if (stringValue is null)
{
isEmptyStringOrNull = true;
return null; return null;
}
if (!GetValue(enumType, stringValue, out var result)) if (!GetValue(enumType, stringValue, out var result))
{ {
if (string.IsNullOrWhiteSpace(stringValue)) if (string.IsNullOrWhiteSpace(stringValue))
{ {
isEmptyString = true; isEmptyStringOrNull = true;
} }
else else
{ {
@ -292,6 +309,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif #endif
} }
internal static void Reset()
{
_undefinedEnumValue = null;
_unknownValuesWarned = new ConcurrentBag<string>();
}
/// <summary> /// <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 /// 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> /// </summary>

View File

@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // Start array reader.Read(); // Start array
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal(); var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
reader.Read(); reader.Read();
if (reader.TokenType != JsonTokenType.EndArray) if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // End array reader.Read(); // End array

View File

@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // Start array reader.Read(); // Start array
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!); var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
reader.Read(); reader.Read();
if (reader.TokenType != JsonTokenType.EndArray) if (reader.TokenType != JsonTokenType.EndArray)
throw new Exception(""); throw new Exception("Invalid JSON structure");
reader.Read(); // End array reader.Read(); // End array

View File

@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>10.7.0</PackageVersion> <PackageVersion>11.0.3</PackageVersion>
<AssemblyVersion>10.7.0</AssemblyVersion> <AssemblyVersion>11.0.3</AssemblyVersion>
<FileVersion>10.7.0</FileVersion> <FileVersion>11.0.3</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>

View File

@ -531,5 +531,50 @@ namespace CryptoExchange.Net
// Unknown decimal format, return null // Unknown decimal format, return null
return null; return null;
} }
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
public static string BytesToHexString(byte[] buff)
=> BytesToHexString(new ArraySegment<byte>(buff));
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
public static string BytesToHexString(ArraySegment<byte> buff)
{
#if NET9_0_OR_GREATER
return Convert.ToHexString(buff);
#else
var result = string.Empty;
foreach (var t in buff)
result += t.ToString("X2");
return result;
#endif
}
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
public static byte[] HexToBytesString(string hexString)
{
if (hexString.StartsWith("0x"))
hexString = hexString.Substring(2);
byte[] bytes = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i += 2)
{
string hexSubstring = hexString.Substring(i, 2);
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
}
return bytes;
}
} }
} }

View File

@ -75,7 +75,7 @@ namespace CryptoExchange.Net
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
bool firstArrayValue = true; bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value) foreach (var entry in (Array)parameter.Value)
{ {
if (!firstArrayValue) if (!firstArrayValue)
uriString.Append('&'); uriString.Append('&');
@ -92,7 +92,7 @@ namespace CryptoExchange.Net
else if (serializationType == ArrayParametersSerialization.MultipleValues) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
bool firstArrayValue = true; bool firstArrayValue = true;
foreach (var entry in (object[])parameter.Value) foreach (var entry in (Array)parameter.Value)
{ {
if (!firstArrayValue) if (!firstArrayValue)
uriString.Append('&'); uriString.Append('&');
@ -109,7 +109,7 @@ namespace CryptoExchange.Net
{ {
uriString.Append('['); uriString.Append('[');
var firstArrayEntry = true; var firstArrayEntry = true;
foreach (var entry in (object[])parameter.Value) foreach (var entry in (Array)parameter.Value)
{ {
if (!firstArrayEntry) if (!firstArrayEntry)
uriString.Append(','); uriString.Append(',');

View File

@ -15,11 +15,6 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary> /// </summary>
string BaseAddress { get; } string BaseAddress { get; }
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary> /// <summary>
/// Format a base and quote asset to an exchange accepted symbol /// Format a base and quote asset to an exchange accepted symbol
/// </summary> /// </summary>
@ -29,19 +24,5 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param> /// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
/// <returns></returns> /// <returns></returns>
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set the API credentials for this API client
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <typeparam name="T">Api credentials type</typeparam>
/// <param name="options">Options to set</param>
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
} }
} }

View File

@ -1,17 +0,0 @@
using System;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Client for accessing REST API's for different exchanges
/// </summary>
public interface ICryptoRestClient
{
/// <summary>
/// Try get
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}

View File

@ -1,17 +0,0 @@
using System;
namespace CryptoExchange.Net.Interfaces.Clients
{
/// <summary>
/// Client for accessing Websocket API's for different exchanges
/// </summary>
public interface ICryptoSocketClient
{
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}

View File

@ -1,4 +1,7 @@
namespace CryptoExchange.Net.Interfaces.Clients using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces.Clients
{ {
/// <summary> /// <summary>
/// Base rest API client /// Base rest API client
@ -15,4 +18,25 @@
/// </summary> /// </summary>
int TotalRequestsMade { get; set; } int TotalRequestsMade { get; set; }
} }
/// <inheritdoc />
public interface IRestApiClient<TApiCredentials> : IRestApiClient
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
void SetApiCredentials(TApiCredentials credentials);
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <param name="options">Options to set</param>
void SetOptions(UpdateOptions<TApiCredentials> options);
}
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces.Clients namespace CryptoExchange.Net.Interfaces.Clients
@ -6,7 +7,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <summary> /// <summary>
/// Base class for rest API implementations /// Base class for rest API implementations
/// </summary> /// </summary>
public interface IRestClient: IDisposable public interface IRestClient : IDisposable
{ {
/// <summary> /// <summary>
/// The options provided for this client /// The options provided for this client
@ -27,5 +28,27 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// Whether client is disposed /// Whether client is disposed
/// </summary> /// </summary>
bool Disposed { get; } 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 />
public interface IRestClient<TApiCredentials> : IRestClient where TApiCredentials : ApiCredentials
{
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(TApiCredentials credentials);
/// <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<TApiCredentials> options);
} }
} }

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default.Interfaces; using CryptoExchange.Net.Sockets.Default.Interfaces;
@ -10,7 +11,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <summary> /// <summary>
/// Socket API client /// Socket API client
/// </summary> /// </summary>
public interface ISocketApiClient: IBaseApiClient public interface ISocketApiClient : IBaseApiClient
{ {
/// <summary> /// <summary>
/// The current amount of socket connections on the API client /// The current amount of socket connections on the API client
@ -73,4 +74,25 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <returns></returns> /// <returns></returns>
Task<CallResult> PrepareConnectionsAsync(); Task<CallResult> PrepareConnectionsAsync();
} }
/// <inheritdoc />
public interface ISocketApiClient<TApiCredentials> : ISocketApiClient
where TApiCredentials : ApiCredentials
{
/// <summary>
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
void SetApiCredentials(TApiCredentials credentials);
/// <summary>
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
/// </summary>
/// <param name="options">Options to set</param>
void SetOptions(UpdateOptions<TApiCredentials> options);
}
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
@ -8,7 +9,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <summary> /// <summary>
/// Base class for socket API implementations /// Base class for socket API implementations
/// </summary> /// </summary>
public interface ISocketClient: IDisposable public interface ISocketClient : IDisposable
{ {
/// <summary> /// <summary>
/// The exchange name /// The exchange name
@ -59,5 +60,28 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
Task UnsubscribeAllAsync(); 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 />
public interface ISocketClient<TApiCredentials> : ISocketClient where TApiCredentials : ApiCredentials
{
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(TApiCredentials credentials);
/// <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<TApiCredentials> options);
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -43,9 +44,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Set string content /// Set string content
/// </summary> /// </summary>
/// <param name="data"></param> void SetContent(string data, Encoding? encoding, string contentType);
/// <param name="contentType"></param>
void SetContent(string data, string contentType);
/// <summary> /// <summary>
/// Add a header to the request /// Add a header to the request

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using System; using System;
using System.Net.Http; using System.Net.Http;

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;

View File

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

View File

@ -130,7 +130,8 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// Default error info /// Default error info
/// </summary> /// </summary>
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint"); protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false,
"No credentials provided for private endpoint, set the `ApiCredentials` option in the client configuration");
/// <summary> /// <summary>
/// ctor /// ctor

View File

@ -1,6 +1,4 @@
using CryptoExchange.Net.Authentication; namespace CryptoExchange.Net.Objects.Options
namespace CryptoExchange.Net.Objects.Options
{ {
/// <summary> /// <summary>
/// Options for API usage /// Options for API usage
@ -17,10 +15,5 @@ namespace CryptoExchange.Net.Objects.Options
/// Note that this comes at a performance cost /// Note that this comes at a performance cost
/// </summary> /// </summary>
public bool? OutputOriginalData { get; set; } public bool? OutputOriginalData { get; set; }
/// <summary>
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
} }
} }

View File

@ -28,11 +28,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20); public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary> /// <summary>
/// Whether or not client side rate limiting should be applied /// Whether or not client side rate limiting should be applied
/// </summary> /// </summary>
@ -45,7 +40,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}"; return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
} }
} }
} }

View File

@ -6,15 +6,10 @@ namespace CryptoExchange.Net.Objects.Options
/// <summary> /// <summary>
/// Library options /// Library options
/// </summary> /// </summary>
/// <typeparam name="TRestOptions"></typeparam> public class LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
/// <typeparam name="TSocketOptions"></typeparam> where TRestOptions : RestExchangeOptions<TEnvironment>, new()
/// <typeparam name="TApiCredentials"></typeparam> where TSocketOptions : SocketExchangeOptions<TEnvironment>, new()
/// <typeparam name="TEnvironment"></typeparam> where TEnvironment : TradeEnvironment
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
where TRestOptions: RestExchangeOptions, new()
where TSocketOptions: SocketExchangeOptions, new()
where TApiCredentials: ApiCredentials
where TEnvironment: TradeEnvironment
{ {
/// <summary> /// <summary>
/// Rest client options /// Rest client options
@ -31,11 +26,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public TEnvironment? Environment { get; set; } public TEnvironment? Environment { get; set; }
/// <summary>
/// The api credentials used for signing requests.
/// </summary>
public TApiCredentials? ApiCredentials { get; set; }
/// <summary> /// <summary>
/// The DI service lifetime for the socket client /// The DI service lifetime for the socket client
/// </summary> /// </summary>
@ -44,9 +34,8 @@ namespace CryptoExchange.Net.Objects.Options
/// <summary> /// <summary>
/// Copy values from these options to the target options /// Copy values from these options to the target options
/// </summary> /// </summary>
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment> public T Set<T>(T targetOptions) where T : LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
{ {
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
targetOptions.Environment = Environment; targetOptions.Environment = Environment;
targetOptions.SocketClientLifeTime = SocketClientLifeTime; targetOptions.SocketClientLifeTime = SocketClientLifeTime;
targetOptions.Rest = Rest.Set(targetOptions.Rest); targetOptions.Rest = Rest.Set(targetOptions.Rest);
@ -55,4 +44,29 @@ namespace CryptoExchange.Net.Objects.Options
return targetOptions; return targetOptions;
} }
} }
/// <summary>
/// Library options
/// </summary>
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment> : LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
where TRestOptions: RestExchangeOptions<TEnvironment, TApiCredentials>, new()
where TSocketOptions: SocketExchangeOptions<TEnvironment, TApiCredentials>, new()
where TApiCredentials: ApiCredentials
where TEnvironment: TradeEnvironment
{
/// <summary>
/// The api credentials used for signing requests.
/// </summary>
public TApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Copy values from these options to the target options
/// </summary>
public new T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
{
targetOptions = base.Set(targetOptions);
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
return targetOptions;
}
}
} }

View File

@ -18,27 +18,10 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public T Set<T>(T item) where T : RestApiOptions, new() public T Set<T>(T item) where T : RestApiOptions, new()
{ {
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData; item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp; item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval; item.TimestampRecalculationInterval = TimestampRecalculationInterval;
return item; return item;
} }
} }
/// <summary>
/// Http API options
/// </summary>
/// <typeparam name="TApiCredentials"></typeparam>
public class RestApiOptions<TApiCredentials>: RestApiOptions where TApiCredentials: ApiCredentials
{
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public new TApiCredentials? ApiCredentials
{
get => (TApiCredentials?)base.ApiCredentials;
set => base.ApiCredentials = value;
}
}
} }

View File

@ -6,8 +6,9 @@ namespace CryptoExchange.Net.Objects.Options
/// <summary> /// <summary>
/// Options for a rest exchange client /// Options for a rest exchange client
/// </summary> /// </summary>
public class RestExchangeOptions: ExchangeOptions public class RestExchangeOptions : ExchangeOptions
{ {
/// <summary> /// <summary>
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
/// </summary> /// </summary>
@ -64,7 +65,6 @@ namespace CryptoExchange.Net.Objects.Options
item.OutputOriginalData = OutputOriginalData; item.OutputOriginalData = OutputOriginalData;
item.AutoTimestamp = AutoTimestamp; item.AutoTimestamp = AutoTimestamp;
item.TimestampRecalculationInterval = TimestampRecalculationInterval; item.TimestampRecalculationInterval = TimestampRecalculationInterval;
item.ApiCredentials = ApiCredentials?.Copy();
item.Proxy = Proxy; item.Proxy = Proxy;
item.RequestTimeout = RequestTimeout; item.RequestTimeout = RequestTimeout;
item.RateLimiterEnabled = RateLimiterEnabled; item.RateLimiterEnabled = RateLimiterEnabled;
@ -83,11 +83,9 @@ namespace CryptoExchange.Net.Objects.Options
} }
} }
/// <summary> /// <inheritdoc />
/// Options for a rest exchange client public class RestExchangeOptions<TEnvironment> : RestExchangeOptions
/// </summary> where TEnvironment : TradeEnvironment
/// <typeparam name="TEnvironment"></typeparam>
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
{ {
/// <summary> /// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
@ -108,20 +106,32 @@ namespace CryptoExchange.Net.Objects.Options
} }
} }
/// <summary> /// <inheritdoc />
/// Options for a rest exchange client public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment>
/// </summary> where TEnvironment : TradeEnvironment
/// <typeparam name="TEnvironment"></typeparam> where TApiCredentials : ApiCredentials
/// <typeparam name="TApiCredentials"></typeparam>
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
{ {
/// <summary> /// <summary>
/// The api credentials used for signing requests to this API. /// The api credentials used for signing requests to this API.
/// </summary> /// </summary>
public new TApiCredentials? ApiCredentials public TApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Set the values of this options on the target options
/// </summary>
public new T Set<T>(T item) where T : RestExchangeOptions<TEnvironment, TApiCredentials>, new()
{ {
get => (TApiCredentials?)base.ApiCredentials; base.Set(item);
set => base.ApiCredentials = value; item.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
return item;
} }
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
} }
} }

View File

@ -24,7 +24,6 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public T Set<T>(T item) where T : SocketApiOptions, new() public T Set<T>(T item) where T : SocketApiOptions, new()
{ {
item.ApiCredentials = ApiCredentials?.Copy();
item.OutputOriginalData = OutputOriginalData; item.OutputOriginalData = OutputOriginalData;
item.SocketNoDataTimeout = SocketNoDataTimeout; item.SocketNoDataTimeout = SocketNoDataTimeout;
item.AutoTimestamp = AutoTimestamp; item.AutoTimestamp = AutoTimestamp;
@ -32,20 +31,4 @@ namespace CryptoExchange.Net.Objects.Options
return item; return item;
} }
} }
/// <summary>
/// Socket API options
/// </summary>
/// <typeparam name="TApiCredentials"></typeparam>
public class SocketApiOptions<TApiCredentials> : SocketApiOptions where TApiCredentials : ApiCredentials
{
/// <summary>
/// The api credentials used for signing requests to this API.
/// </summary>
public new TApiCredentials? ApiCredentials
{
get => (TApiCredentials?)base.ApiCredentials;
set => base.ApiCredentials = value;
}
}
} }

View File

@ -91,7 +91,6 @@ namespace CryptoExchange.Net.Objects.Options
/// <returns></returns> /// <returns></returns>
public T Set<T>(T item) where T : SocketExchangeOptions, new() public T Set<T>(T item) where T : SocketExchangeOptions, new()
{ {
item.ApiCredentials = ApiCredentials?.Copy();
item.AutoTimestamp = AutoTimestamp; item.AutoTimestamp = AutoTimestamp;
item.OutputOriginalData = OutputOriginalData; item.OutputOriginalData = OutputOriginalData;
item.ReconnectPolicy = ReconnectPolicy; item.ReconnectPolicy = ReconnectPolicy;
@ -110,12 +109,11 @@ namespace CryptoExchange.Net.Objects.Options
} }
} }
/// <summary> /// <inheritdoc />
/// Options for a socket exchange client public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions
/// </summary> where TEnvironment : TradeEnvironment
/// <typeparam name="TEnvironment"></typeparam>
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
{ {
/// <summary> /// <summary>
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
@ -138,17 +136,29 @@ namespace CryptoExchange.Net.Objects.Options
/// <summary> /// <summary>
/// Options for a socket exchange client /// Options for a socket exchange client
/// </summary> /// </summary>
/// <typeparam name="TEnvironment"></typeparam> public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment>
/// <typeparam name="TApiCredentials"></typeparam> where TEnvironment : TradeEnvironment
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials where TApiCredentials : ApiCredentials
{ {
/// <summary> /// <summary>
/// The api credentials used for signing requests to this API. /// The api credentials used for signing requests to this API.
/// </summary> /// </summary>
public new TApiCredentials? ApiCredentials public TApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Set the values of this options on the target options
/// </summary>
public new T Set<T>(T item) where T : SocketExchangeOptions<TEnvironment, TApiCredentials>, new()
{ {
get => (TApiCredentials?)base.ApiCredentials; base.Set(item);
set => base.ApiCredentials = value; item.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
return item;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
} }
} }
} }

View File

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

View File

@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public int? ConnectionId { get; set; } public int? ConnectionId { get; set; }
/// <summary>
/// Whether the endpoint path should always include the trailing `/`
/// </summary>
public bool? ForcePathEndWithSlash { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>

View File

@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="arraySerialization">Array serialization type</param> /// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param> /// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param> /// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
HttpMethod method, HttpMethod method,
@ -59,8 +60,9 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null, bool? preventCaching = null,
bool? tryParseOnNonSuccess = null) bool? tryParseOnNonSuccess = null,
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess); bool? forcePathEndWithSlash = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
/// <summary> /// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache /// Get a definition if it is already in the cache or create a new definition and add it to the cache
@ -77,6 +79,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="arraySerialization">Array serialization type</param> /// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param> /// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param> /// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
string identifier, string identifier,
@ -90,7 +93,8 @@ namespace CryptoExchange.Net.Objects
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null, bool? preventCaching = null,
bool? tryParseOnNonSuccess = null) bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null)
{ {
if (!_definitions.TryGetValue(identifier, out var def)) if (!_definitions.TryGetValue(identifier, out var def))
@ -105,7 +109,8 @@ namespace CryptoExchange.Net.Objects
RequestBodyFormat = requestBodyFormat, RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false, PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
}; };
_definitions.TryAdd(identifier, def); _definitions.TryAdd(identifier, def);
} }

View File

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

View File

@ -19,9 +19,6 @@ namespace CryptoExchange.Net.Requests
/// <summary> /// <summary>
/// Create request object for web request /// Create request object for web request
/// </summary> /// </summary>
/// <param name="request"></param>
/// <param name="client"></param>
/// <param name="requestId"></param>
public Request(HttpRequestMessage request, HttpClient client, int requestId) public Request(HttpRequestMessage request, HttpClient client, int requestId)
{ {
_httpClient = client; _httpClient = client;
@ -55,10 +52,12 @@ namespace CryptoExchange.Net.Requests
public int RequestId { get; } public int RequestId { get; }
/// <inheritdoc /> /// <inheritdoc />
public void SetContent(string data, string contentType) public void SetContent(string data, Encoding? encoding, string contentType)
{ {
Content = data; Content = data;
_request.Content = new StringContent(data, Encoding.UTF8, contentType); _request.Content = new StringContent(data, encoding ?? Encoding.UTF8, contentType);
if (encoding == null)
_request.Content.Headers.ContentType!.CharSet = null;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;

View File

@ -16,6 +16,11 @@
/// <summary> /// <summary>
/// Order has been canceled /// Order has been canceled
/// </summary> /// </summary>
Canceled Canceled,
/// <summary>
/// Unknown/unmapped status
/// </summary>
Unknown
} }
} }

View File

@ -16,6 +16,11 @@
/// <summary> /// <summary>
/// Completed /// Completed
/// </summary> /// </summary>
Completed Completed,
/// <summary>
/// Unknown/unmapped status
/// </summary>
Unknown
} }
} }

View File

@ -20,6 +20,11 @@
/// <summary> /// <summary>
/// Trigger order has been triggered. Resulting order might be filled or not. /// Trigger order has been triggered. Resulting order might be filled or not.
/// </summary> /// </summary>
Triggered Triggered,
/// <summary>
/// Unknown/unmapped status
/// </summary>
Unknown
} }
} }

View File

@ -8,7 +8,9 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces; using CryptoExchange.Net.Sockets.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net.WebSockets; using System.Net.WebSockets;
@ -123,15 +125,7 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
get get
{ {
try return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
{
_listenersLock.EnterReadLock();
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
}
finally
{
_listenersLock.ExitReadLock();
}
} }
} }
@ -142,16 +136,7 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
get get
{ {
try return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
{
_listenersLock.EnterReadLock();
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
}
finally
{
_listenersLock.ExitReadLock();
}
} }
} }
@ -255,15 +240,7 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
get get
{ {
try return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToArray()!;
{
_listenersLock.EnterReadLock();
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToArray()!;
}
finally
{
_listenersLock.ExitReadLock();
}
} }
} }
@ -274,22 +251,18 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
get get
{ {
try return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
{
_listenersLock.EnterReadLock();
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
}
finally
{
_listenersLock.ExitReadLock();
}
} }
} }
private bool _pausedActivity; private bool _pausedActivity;
private readonly ReaderWriterLockSlim _listenersLock = new ReaderWriterLockSlim(); #if NET9_0_OR_GREATER
private readonly List<IMessageProcessor> _listeners; private readonly Lock _listenersLock = new Lock();
#else
private readonly object _listenersLock = new object();
#endif
private ReadOnlyCollection<IMessageProcessor> _listeners;
private readonly ILogger _logger; private readonly ILogger _logger;
private SocketStatus _status; private SocketStatus _status;
@ -338,7 +311,7 @@ namespace CryptoExchange.Net.Sockets.Default
_socket.OnError += HandleErrorAsync; _socket.OnError += HandleErrorAsync;
_socket.GetReconnectionUrl = GetReconnectionUrlAsync; _socket.GetReconnectionUrl = GetReconnectionUrlAsync;
_listeners = new List<IMessageProcessor>(); _listeners = new ReadOnlyCollection<IMessageProcessor>([]);
_serializer = apiClient.CreateSerializer(); _serializer = apiClient.CreateSerializer();
} }
@ -365,25 +338,17 @@ namespace CryptoExchange.Net.Sockets.Default
if (ApiClient._socketConnections.ContainsKey(SocketId)) if (ApiClient._socketConnections.ContainsKey(SocketId))
ApiClient._socketConnections.TryRemove(SocketId, out _); ApiClient._socketConnections.TryRemove(SocketId, out _);
try foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
{ {
_listenersLock.EnterWriteLock(); subscription.IsClosingConnection = true;
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection)) subscription.Reset();
{ }
subscription.IsClosingConnection = true;
subscription.Reset();
}
foreach (var query in _listeners.OfType<Query>().ToList()) var queryList = _listeners.OfType<Query>().ToList();
{ foreach (var query in queryList)
query.Fail(new WebError("Connection interrupted")); query.Fail(new WebError("Connection interrupted"));
_listeners.Remove(query);
} RemoveMessageProcessors(queryList);
}
finally
{
_listenersLock.ExitWriteLock();
}
_ = Task.Run(() => ConnectionClosed?.Invoke()); _ = Task.Run(() => ConnectionClosed?.Invoke());
return Task.CompletedTask; return Task.CompletedTask;
@ -399,22 +364,14 @@ namespace CryptoExchange.Net.Sockets.Default
Authenticated = false; Authenticated = false;
_lastSequenceNumber = 0; _lastSequenceNumber = 0;
try foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
{ subscription.Reset();
_listenersLock.EnterWriteLock();
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
subscription.Reset();
foreach (var query in _listeners.OfType<Query>().ToList()) var queryList = _listeners.OfType<Query>().ToList();
{ foreach (var query in queryList)
query.Fail(new WebError("Connection interrupted")); query.Fail(new WebError("Connection interrupted"));
_listeners.Remove(query);
} RemoveMessageProcessors(queryList);
}
finally
{
_listenersLock.ExitWriteLock();
}
_ = Task.Run(() => ConnectionLost?.Invoke()); _ = Task.Run(() => ConnectionLost?.Invoke());
return Task.CompletedTask; return Task.CompletedTask;
@ -436,19 +393,11 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
Status = SocketStatus.Resubscribing; Status = SocketStatus.Resubscribing;
try var queryList = _listeners.OfType<Query>().ToList();
{ foreach (var query in queryList)
_listenersLock.EnterWriteLock(); query.Fail(new WebError("Connection interrupted"));
foreach (var query in _listeners.OfType<Query>().ToList())
{ RemoveMessageProcessors(queryList);
query.Fail(new WebError("Connection interrupted"));
_listeners.Remove(query);
}
}
finally
{
_listenersLock.ExitWriteLock();
}
// Can't wait for this as it would cause a deadlock // Can't wait for this as it would cause a deadlock
_ = Task.Run(async () => _ = Task.Run(async () =>
@ -503,17 +452,7 @@ namespace CryptoExchange.Net.Sockets.Default
/// <returns></returns> /// <returns></returns>
protected virtual Task HandleRequestRateLimitedAsync(int requestId) protected virtual Task HandleRequestRateLimitedAsync(int requestId)
{ {
Query? query; var query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
try
{
_listenersLock.EnterReadLock();
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
}
finally
{
_listenersLock.ExitReadLock();
}
if (query == null) if (query == null)
return Task.CompletedTask; return Task.CompletedTask;
@ -537,17 +476,7 @@ namespace CryptoExchange.Net.Sockets.Default
/// <param name="requestId">Id of the request sent</param> /// <param name="requestId">Id of the request sent</param>
protected virtual Task HandleRequestSentAsync(int requestId) protected virtual Task HandleRequestSentAsync(int requestId)
{ {
Query? query; var query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
try
{
_listenersLock.EnterReadLock();
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
}
finally
{
_listenersLock.ExitReadLock();
}
if (query == null) if (query == null)
return Task.CompletedTask; return Task.CompletedTask;
@ -593,27 +522,19 @@ namespace CryptoExchange.Net.Sockets.Default
} }
Type? deserializationType = null; Type? deserializationType = null;
try foreach (var subscription in _listeners)
{ {
_listenersLock.EnterReadLock(); foreach (var route in subscription.MessageRouter.Routes)
foreach (var subscription in _listeners)
{ {
foreach (var route in subscription.MessageRouter.Routes) if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal))
{ continue;
if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal))
continue;
deserializationType = route.DeserializationType; deserializationType = route.DeserializationType;
break; break;
}
if (deserializationType != null)
break;
} }
}
finally if (deserializationType != null)
{ break;
_listenersLock.ExitReadLock();
} }
if (deserializationType == null) if (deserializationType == null)
@ -660,89 +581,69 @@ namespace CryptoExchange.Net.Sockets.Default
var topicFilter = messageConverter.GetTopicFilter(result); var topicFilter = messageConverter.GetTopicFilter(result);
bool processed = false; bool processed = false;
try foreach (var processor in _listeners)
{ {
_listenersLock.EnterReadLock(); bool isQuery = false;
var currentCount = _listeners.Count; Query? query = null;
for(var i = 0; i < _listeners.Count; i++) if (processor is Query cquery)
{ {
if (_listeners.Count != currentCount) isQuery = true;
{ query = cquery;
// Possible a query added or removed. If added it's not a problem, if removed it is
if (_listeners.Count < currentCount)
throw new Exception("Listeners list adjusted, can't continue processing");
}
var processor = _listeners[i];
bool isQuery = false;
Query? query = null;
if (processor is Query cquery)
{
isQuery = true;
query = cquery;
}
var complete = false;
foreach (var route in processor.MessageRouter.Routes)
{
if (route.TypeIdentifier != typeIdentifier)
continue;
// Forward message rules:
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
// | N | N | - | Y | No topic filter applied
// | N | Y | - | N | Route only listens to specific topic
// | Y | N | - | Y | Route listens to all message regardless of topic
// | Y | Y | Y | Y | Route listens to specific message topic
// | Y | Y | N | N | Route listens to different topic
if (topicFilter == null)
{
if (route.TopicFilter != null)
// No topic on message, but route is filtering on topic
continue;
}
else
{
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
// Message has a topic, and the route has a filter for another topic
continue;
}
processed = true;
if (isQuery && query!.Completed)
continue;
processor.Handle(this, receiveTime, originalData, result, route);
if (isQuery && !route.MultipleReaders)
{
complete = true;
break;
}
}
if (complete)
break;
} }
}
finally var complete = false;
{
_listenersLock.ExitReadLock(); foreach (var route in processor.MessageRouter.Routes)
{
if (route.TypeIdentifier != typeIdentifier)
continue;
// Forward message rules:
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
// | N | N | - | Y | No topic filter applied
// | N | Y | - | N | Route only listens to specific topic
// | Y | N | - | Y | Route listens to all message regardless of topic
// | Y | Y | Y | Y | Route listens to specific message topic
// | Y | Y | N | N | Route listens to different topic
if (topicFilter == null)
{
if (route.TopicFilter != null)
// No topic on message, but route is filtering on topic
continue;
}
else
{
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
// Message has a topic, and the route has a filter for another topic
continue;
}
processed = true;
if (isQuery && query!.Completed)
continue;
processor.Handle(this, receiveTime, originalData, result, route);
if (isQuery && !route.MultipleReaders)
{
complete = true;
break;
}
}
if (complete)
break;
} }
if (!processed) if (!processed)
{ {
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data)) if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
{ {
lock (_listenersLock) _logger.ReceivedMessageNotMatchedToAnyListener(
{ SocketId,
_logger.ReceivedMessageNotMatchedToAnyListener( typeIdentifier,
SocketId, topicFilter!,
typeIdentifier, string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
topicFilter!,
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
}
} }
} }
} }
@ -787,18 +688,10 @@ namespace CryptoExchange.Net.Sockets.Default
if (ApiClient._socketConnections.ContainsKey(SocketId)) if (ApiClient._socketConnections.ContainsKey(SocketId))
ApiClient._socketConnections.TryRemove(SocketId, out _); ApiClient._socketConnections.TryRemove(SocketId, out _);
try foreach (var subscription in _listeners.OfType<Subscription>())
{ {
_listenersLock.EnterReadLock(); if (subscription.CancellationTokenRegistration.HasValue)
foreach (var subscription in _listeners.OfType<Subscription>()) subscription.CancellationTokenRegistration.Value.Dispose();
{
if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose();
}
}
finally
{
_listenersLock.ExitReadLock();
} }
await _socket.CloseAsync().ConfigureAwait(false); await _socket.CloseAsync().ConfigureAwait(false);
@ -828,32 +721,12 @@ namespace CryptoExchange.Net.Sockets.Default
if (subscription.CancellationTokenRegistration.HasValue) if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose(); subscription.CancellationTokenRegistration.Value.Dispose();
bool anyDuplicateSubscription; bool anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
bool shouldCloseConnection; bool shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
try
{
_listenersLock.EnterReadLock();
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
}
finally
{
_listenersLock.ExitReadLock();
}
if (!anyDuplicateSubscription) if (!anyDuplicateSubscription)
{ {
bool needUnsub; var needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
try
{
_listenersLock.EnterReadLock();
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
}
finally
{
_listenersLock.ExitReadLock();
}
if (needUnsub && _socket.IsOpen) if (needUnsub && _socket.IsOpen)
await UnsubscribeAsync(subscription).ConfigureAwait(false); await UnsubscribeAsync(subscription).ConfigureAwait(false);
} }
@ -877,15 +750,7 @@ namespace CryptoExchange.Net.Sockets.Default
await CloseAsync().ConfigureAwait(false); await CloseAsync().ConfigureAwait(false);
} }
try RemoveMessageProcessor(subscription);
{
_listenersLock.EnterWriteLock();
_listeners.Remove(subscription);
}
finally
{
_listenersLock.ExitWriteLock();
}
subscription.Status = SubscriptionStatus.Closed; subscription.Status = SubscriptionStatus.Closed;
} }
@ -908,15 +773,8 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
if (Status != SocketStatus.None && Status != SocketStatus.Connected) if (Status != SocketStatus.None && Status != SocketStatus.Connected)
return false; return false;
try
{ AddMessageProcessor(subscription);
_listenersLock.EnterWriteLock();
_listeners.Add(subscription);
}
finally
{
_listenersLock.ExitWriteLock();
}
if (subscription.UserSubscription) if (subscription.UserSubscription)
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount); _logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
@ -929,15 +787,7 @@ namespace CryptoExchange.Net.Sockets.Default
/// <param name="id"></param> /// <param name="id"></param>
public Subscription? GetSubscription(int id) public Subscription? GetSubscription(int id)
{ {
try return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
{
_listenersLock.EnterReadLock();
return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
}
finally
{
_listenersLock.ExitReadLock();
}
} }
/// <summary> /// <summary>
@ -985,29 +835,12 @@ namespace CryptoExchange.Net.Sockets.Default
private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default) private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default)
{ {
try AddMessageProcessor(query);
{
_listenersLock.EnterWriteLock();
_listeners.Add(query);
}
finally
{
_listenersLock.ExitWriteLock();
}
var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false); var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false);
if (!sendResult) if (!sendResult)
{ {
query.Fail(sendResult.Error!); query.Fail(sendResult.Error!);
try RemoveMessageProcessor(query);
{
_listenersLock.EnterWriteLock();
_listeners.Remove(query);
}
finally
{
_listenersLock.ExitWriteLock();
}
return; return;
} }
@ -1038,15 +871,7 @@ namespace CryptoExchange.Net.Sockets.Default
} }
finally finally
{ {
try RemoveMessageProcessor(query);
{
_listenersLock.EnterWriteLock();
_listeners.Remove(query);
}
finally
{
_listenersLock.ExitWriteLock();
}
} }
} }
@ -1152,17 +977,7 @@ namespace CryptoExchange.Net.Sockets.Default
if (!DedicatedRequestConnection.IsDedicatedRequestConnection) if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
{ {
bool anySubscriptions; var anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
try
{
_listenersLock.EnterReadLock();
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
}
finally
{
_listenersLock.ExitReadLock();
}
if (!anySubscriptions) if (!anySubscriptions)
{ {
// No need to resubscribe anything // No need to resubscribe anything
@ -1172,18 +987,8 @@ namespace CryptoExchange.Net.Sockets.Default
} }
} }
bool anyAuthenticated; bool anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
try
{
_listenersLock.EnterReadLock();
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|| DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated; || DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated;
}
finally
{
_listenersLock.ExitReadLock();
}
if (anyAuthenticated) if (anyAuthenticated)
{ {
// If we reconnected a authenticated connection we need to re-authenticate // If we reconnected a authenticated connection we need to re-authenticate
@ -1206,17 +1011,7 @@ namespace CryptoExchange.Net.Sockets.Default
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult(new WebError("Socket not connected")); return new CallResult(new WebError("Socket not connected"));
List<Subscription> subList; var subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
try
{
_listenersLock.EnterReadLock();
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
}
finally
{
_listenersLock.ExitReadLock();
}
if (subList.Count == 0) if (subList.Count == 0)
break; break;
@ -1398,6 +1193,37 @@ namespace CryptoExchange.Net.Sockets.Default
}); });
} }
private void AddMessageProcessor(IMessageProcessor processor)
{
lock (_listenersLock)
{
var updatedList = new List<IMessageProcessor>(_listeners);
updatedList.Add(processor);
_listeners = updatedList.AsReadOnly();
}
}
private void RemoveMessageProcessor(IMessageProcessor processor)
{
lock (_listenersLock)
{
var updatedList = new List<IMessageProcessor>(_listeners);
updatedList.Remove(processor);
_listeners = updatedList.AsReadOnly();
}
}
private void RemoveMessageProcessors(IEnumerable<IMessageProcessor> processors)
{
lock (_listenersLock)
{
var updatedList = new List<IMessageProcessor>(_listeners);
foreach (var processor in processors)
updatedList.Remove(processor);
_listeners = updatedList.AsReadOnly();
}
}
} }
} }

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default.Interfaces; using CryptoExchange.Net.Sockets.Default.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

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

View File

@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Testing.Implementations
Content = Encoding.UTF8.GetString(data); Content = Encoding.UTF8.GetString(data);
} }
public void SetContent(string data, string contentType) public void SetContent(string data, Encoding? encoding, string contentType)
{ {
Content = data; Content = data;
} }

View File

@ -112,6 +112,8 @@ namespace CryptoExchange.Net.Testing.Implementations
public async Task ReconnectAsync() public async Task ReconnectAsync()
{ {
await Task.Delay(1).ConfigureAwait(false);
if (OnReconnecting != null) if (OnReconnecting != null)
await OnReconnecting().ConfigureAwait(false); await OnReconnecting().ConfigureAwait(false);

View File

@ -1,7 +1,9 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing.Comparers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -59,7 +61,17 @@ namespace CryptoExchange.Net.Testing
/// <typeparam name="T">Type of response</typeparam> /// <typeparam name="T">Type of response</typeparam>
/// <param name="expression">The call expression</param> /// <param name="expression">The call expression</param>
/// <param name="authRequest">Whether this is an authenticated request</param> /// <param name="authRequest">Whether this is an authenticated request</param>
public async Task RunAndCheckResult<T>(Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest) /// <param name="checkMissingFields">Whether to check the response for missing fields</param>
/// <param name="compareNestedProperty">Nested property to use for comparing when checking for missing fields</param>
/// <param name="ignoreProperties">Properties to ignore when checking for missing fields</param>
/// <param name="useSingleArrayItem">Whether to use the single array item as compare when checking for missing fields</param>
public async Task RunAndCheckResult<T>(
Expression<Func<TClient, Task<WebCallResult<T>>>> expression,
bool authRequest,
bool checkMissingFields = false,
string? compareNestedProperty = null,
List<string>? ignoreProperties = null,
bool? useSingleArrayItem = null)
{ {
if (!ShouldRun()) if (!ShouldRun())
return; return;
@ -93,6 +105,22 @@ namespace CryptoExchange.Net.Testing
if (!result.Success) if (!result.Success)
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error); throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
if (checkMissingFields)
{
var data = result.Data;
var originalData = result.OriginalData;
if (originalData == null)
throw new Exception($"Original data needs to be enabled in the client options to check for missing fields");
try {
SystemTextJsonComparer.CompareData(expressionBody.Method.Name, data, originalData, compareNestedProperty, ignoreProperties, useSingleArrayItem ?? false);
}
catch (Exception ex)
{
throw new Exception($"Compare failed: {ex.Message}; original data: {originalData}", ex);
}
}
Debug.WriteLine($"{expressionBody.Method.Name} {result}"); Debug.WriteLine($"{expressionBody.Method.Name} {result}");
} }

View File

@ -0,0 +1,126 @@
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

@ -20,6 +20,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly ExchangeParameters? _exchangeParameters; private readonly ExchangeParameters? _exchangeParameters;
private readonly bool _requiresSymbolParameterOpenOrders; private readonly bool _requiresSymbolParameterOpenOrders;
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new(); private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate; internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
@ -355,17 +356,17 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
DateTime? fromTime = null; DateTime? fromTime = null;
string? source = null; string? source = null;
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer // Use the last timestamp we we received data from the websocket as state should be correct at that time
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value)) if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
{ {
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1); fromTime = _lastDataTimeBeforeDisconnect.Value.Add(-_pollOverlapPeriod);
source = "LastDataTimeBeforeDisconnect"; source = "LastDataTimeBeforeDisconnect";
} }
// If we've previously polled use that timestamp to request data from // If we've previously polled use that timestamp to request data from
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime)) if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
{ {
fromTime = _lastPollTime; fromTime = _lastPollTime.Value.Add(-_pollOverlapPeriod);
source = "LastPollTime"; source = "LastPollTime";
} }
@ -378,7 +379,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
{ {
// Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long // Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long
// history if the open order is far back // history if the open order is far back
fromTime = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1); fromTime = trackedOrdersMinOpenTime.Value.AddSeconds(-1);
source = "OpenOrder"; source = "OpenOrder";
} }
@ -388,7 +389,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
source = "StartTime"; source = "StartTime";
} }
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1)) if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(5))
{ {
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync // Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
fromTime = DateTime.UtcNow.AddSeconds(-5); fromTime = DateTime.UtcNow.AddSeconds(-5);

View File

@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly IFuturesOrderRestClient _restClient; private readonly IFuturesOrderRestClient _restClient;
private readonly IUserTradeSocketClient? _socketClient; private readonly IUserTradeSocketClient? _socketClient;
private readonly ExchangeParameters? _exchangeParameters; private readonly ExchangeParameters? _exchangeParameters;
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
internal Func<string[]>? GetTrackedOrderIds { get; set; } internal Func<string[]>? GetTrackedOrderIds { get; set; }
@ -106,22 +107,22 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private DateTime? GetTradesRequestStartTime() private DateTime? GetTradesRequestStartTime()
{ {
// Determine the timestamp from which we need to check order status // Determine the timestamp from which we need to request trades from
// Use the timestamp we last know the correct state of the data // Use the timestamp we last know the correct state of the data
DateTime? fromTime = null; DateTime? fromTime = null;
string? source = null; string? source = null;
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer // Use the last timestamp we we received data from the websocket as state should be correct at that time.
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value)) if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
{ {
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1); fromTime = _lastDataTimeBeforeDisconnect.Value.Add(-_pollOverlapPeriod);
source = "LastDataTimeBeforeDisconnect"; source = "LastDataTimeBeforeDisconnect";
} }
// If we've previously polled use that timestamp to request data from // If we've previously polled use that timestamp to request data from
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime)) if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
{ {
fromTime = _lastPollTime; fromTime = _lastPollTime.Value.Add(-_pollOverlapPeriod);
source = "LastPollTime"; source = "LastPollTime";
} }
@ -132,7 +133,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
} }
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
if (now - fromTime < TimeSpan.FromSeconds(1)) if (now - fromTime < TimeSpan.FromSeconds(5))
{ {
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync // Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
fromTime = DateTime.UtcNow.AddSeconds(-5); fromTime = DateTime.UtcNow.AddSeconds(-5);

View File

@ -20,6 +20,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly ExchangeParameters? _exchangeParameters; private readonly ExchangeParameters? _exchangeParameters;
private readonly bool _requiresSymbolParameterOpenOrders; private readonly bool _requiresSymbolParameterOpenOrders;
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new(); private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate; internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
@ -366,17 +367,17 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
DateTime? fromTime = null; DateTime? fromTime = null;
string? source = null; string? source = null;
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer // Use the last timestamp we we received data from the websocket as state should be correct at that time.
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value)) if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
{ {
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1); fromTime = _lastDataTimeBeforeDisconnect.Value.Add(-_pollOverlapPeriod);
source = "LastDataTimeBeforeDisconnect"; source = "LastDataTimeBeforeDisconnect";
} }
// If we've previously polled use that timestamp to request data from // If we've previously polled use that timestamp to request data from
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime)) if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
{ {
fromTime = _lastPollTime; fromTime = _lastPollTime.Value.Add(-_pollOverlapPeriod);
source = "LastPollTime"; source = "LastPollTime";
} }
@ -389,7 +390,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
{ {
// Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long // Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long
// history if the open order is far back // history if the open order is far back
fromTime = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1); fromTime = trackedOrdersMinOpenTime.Value.AddSeconds(-1);
source = "OpenOrder"; source = "OpenOrder";
} }
@ -399,7 +400,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
source = "StartTime"; source = "StartTime";
} }
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1)) if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(5))
{ {
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync // Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
fromTime = DateTime.UtcNow.AddSeconds(-5); fromTime = DateTime.UtcNow.AddSeconds(-5);

View File

@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly ISpotOrderRestClient _restClient; private readonly ISpotOrderRestClient _restClient;
private readonly IUserTradeSocketClient? _socketClient; private readonly IUserTradeSocketClient? _socketClient;
private readonly ExchangeParameters? _exchangeParameters; private readonly ExchangeParameters? _exchangeParameters;
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
internal Func<string[]>? GetTrackedOrderIds { get; set; } internal Func<string[]>? GetTrackedOrderIds { get; set; }
@ -103,22 +104,22 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private DateTime? GetTradesRequestStartTime() private DateTime? GetTradesRequestStartTime()
{ {
// Determine the timestamp from which we need to check order status // Determine the timestamp from which we need to request trades from
// Use the timestamp we last know the correct state of the data // Use the timestamp we last know the correct state of the data
DateTime? fromTime = null; DateTime? fromTime = null;
string? source = null; string? source = null;
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer // Use the last timestamp we we received data from the websocket as state should be correct at that time.
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value)) if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
{ {
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1); fromTime = _lastDataTimeBeforeDisconnect.Value.Add(-_pollOverlapPeriod);
source = "LastDataTimeBeforeDisconnect"; source = "LastDataTimeBeforeDisconnect";
} }
// If we've previously polled use that timestamp to request data from // If we've previously polled use that timestamp to request data from
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime)) if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
{ {
fromTime = _lastPollTime; fromTime = _lastPollTime.Value.Add(-_pollOverlapPeriod);
source = "LastPollTime"; source = "LastPollTime";
} }
@ -128,7 +129,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
source = "StartTime"; source = "StartTime";
} }
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1)) if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(5))
{ {
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync // Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
fromTime = DateTime.UtcNow.AddSeconds(-5); fromTime = DateTime.UtcNow.AddSeconds(-5);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,9 +3,11 @@
"ExchangeApiOptions": { "ExchangeApiOptions": {
// API credentials for both REST and Websocket client // API credentials for both REST and Websocket client
"ApiCredentials": { "ApiCredentials": {
"Key": "APIKEY", "HMAC": {
"Secret": "SECRET", "Key": "APIKEY",
"PassPhrase": "Phrase" // Optional passphrase for exchanges which need it "Secret": "SECRET",
"PassPhrase": "Phrase" // Optional passphrase for exchanges which need it
}
}, },
// Set the environment by name // Set the environment by name
"Environment": { "Environment": {

View File

@ -19,6 +19,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%| |![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%| |![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%| |![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|![Bitstamp](https://raw.githubusercontent.com/JKorf/Bitstamp.Net/refs/heads/main/Bitstamp.Net/Icon/icon.png)|Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitstamp.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitstamp.Net)|-|-|
|![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-| |![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-|
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-| |![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-| |![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
@ -67,6 +68,46 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 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
* Version 11.0.0 - 23 Mar 2026
* Updated API credential logic, exchange implementation are expected to provide their own credentials implementation with ApiCredentials as base class
* Removed ApiCredentials implementation used by most exchanges
* Removed ApiCredentialsType Enum
* Added CredentialSet base class and implementations for defining different API credentials
* Added optional type param to AuthenticationProvider for the specific API credential type to improve type safety
* Moved AuthenticationProvider/ApiCredentials from BaseApiClient to RestApiClient/SocketApiClient base classes
* Added optional type params to RestApiClient/SocketApiClient base class to specify the AuthenticationProvider type and credentials type to improve type safety
* Moved SetOptions/SetApiCredentials from BaseApiClient to RestApiClient/SocketApiClient
* Extracted LibraryOptions<TRestOptions, TSocketOptions, TEnvironment> without TApiCredentials for libraries without API credentials
* Removed ApiCredentials from ApiOptions, credentials can only be configured at library, rest or socket level
* Added EnvironmentName property to RestApiClient/SocketApiClient
* Added Unknown enum value to Shared interfaces SharedOrderStatus, SharedTransferStatus and SharedTriggerOrderStatus enums
* Updated Enum converter to map value to an undefined Enum value instead of the first Enum value
* Added support for checking for missing fields on RestIntegrationTest
* Added Web3 signing utilities
* Added BytesToHexString and HexToBytesString to ExchangeHelpers static class
* Fixed bug where WebSocket connections are not reconnected when configuring Proxy with SetUpdates
* Removed legacy CryptoBaseClient, CryptoRestClient and CryptoSocketClient
* Version 10.8.0 - 06 Mar 2026
* Added `RequestBodyContentEncoding` and `OmitContentTypeHeaderWithoutContent` config to RestApiClient
* Added `ForcePathEndWithSlash` setting to RequestDefinition
* Added `encoding` parameter to SetContent on REST Request
* Version 10.7.2 - 02 Mar 2026
* Added small overlap in UserDataTracker polling logic to account for API endpoints not immediately having the data available
* Version 10.7.1 - 25 Feb 2026
* Fixed deadlock scenario in websocket connection when subscribe and handling message concurrently
* Version 10.7.0 - 24 Feb 2026 * Version 10.7.0 - 24 Feb 2026
* Added parsing of REST response data up to 128 characters for error responses * Added parsing of REST response data up to 128 characters for error responses
* Added check for invalid json in JsonSocketMessageHandler * Added check for invalid json in JsonSocketMessageHandler