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

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
This commit is contained in:
Jan Korf 2026-03-23 14:28:00 +01:00 committed by GitHub
parent 875696a73a
commit 2f9f97f4e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1615 additions and 771 deletions

View File

@ -29,7 +29,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 +44,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)
}; };
@ -48,84 +54,9 @@ namespace CryptoExchange.Net.UnitTests
Assert.That(options.ApiCredentials.Secret == "456"); Assert.That(options.ApiCredentials.Secret == "456");
} }
[Test]
public void TestApiOptionsAreSet()
{
// 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 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() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
[Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault()
{
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient(options =>
{
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
options.Environment = new TestEnvironment("Test", "https://test.test");
});
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "333");
Assert.That(authProvider1.GetSecret() == "444");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
} }
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

@ -155,15 +155,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)]
@ -425,6 +432,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

@ -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

@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Authentication.Signing
{ {
/// <summary> /// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields. /// 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. /// 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 /// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary> /// </summary>
public static byte[] EncodeEip721( public static byte[] EncodeEip721(
@ -64,7 +64,7 @@ namespace CryptoExchange.Net.Authentication.Signing
/// <summary> /// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields. /// 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. /// 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 /// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary> /// </summary>
public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData) public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData)
@ -152,7 +152,7 @@ namespace CryptoExchange.Net.Authentication.Signing
if (memberValue.Value is string v) if (memberValue.Value is string v)
{ {
if (!BigInteger.TryParse(v, out BigInteger parsedOutput)) if (!BigInteger.TryParse(v, out BigInteger parsedOutput))
throw new Exception(""); throw new Exception($"Failed to encode BigInteger string {v}");
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput)); writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput));
} }
@ -186,19 +186,11 @@ namespace CryptoExchange.Net.Authentication.Signing
} }
else else
{ {
throw new Exception(); throw new Exception("Unknown number value");
} }
} }
else if (memberValue.TypeName.StartsWith("bytes")) else if (memberValue.TypeName.StartsWith("bytes"))
{ {
// Applicable?
//if (memberValue.Value is string v)
// writer.Write(AbiEncoder.AbiValueEncodeHexBytes(v));
//else if (memberValue.Value is byte[] b)
// writer.Write(AbiEncoder.AbiValueEncodeBytes(b));
//else
// throw new Exception("Unknown byte value type");
var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5)); var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5));
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value)); writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value));
} }

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
{ {
@ -24,5 +28,46 @@ namespace CryptoExchange.Net.Clients
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange"); LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
} }
}
/// <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>
/// 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;
@ -134,4 +136,43 @@ namespace CryptoExchange.Net.Clients
return result; return result;
} }
} }
/// <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>
/// 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;
@ -103,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>
@ -111,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)
@ -214,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());
@ -319,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!;
} }
@ -334,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!;
} }
@ -373,7 +408,7 @@ namespace CryptoExchange.Net.Clients
try try
{ {
AuthenticationProvider?.ProcessRequest(this, requestConfiguration); GetAuthenticationProvider()?.ProcessRequest(this, requestConfiguration);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -689,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)
@ -793,4 +820,162 @@ namespace CryptoExchange.Net.Clients
&& !definition.PreventCaching; && !definition.PreventCaching;
} }
/// <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)
{
_proxyConfigured = options.Proxy != null;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
}
}
/// <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>
@ -1040,4 +1036,169 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType); public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
} }
/// <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)
{
var previousProxyIsSet = _proxyConfigured;
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
_proxyConfigured = options.Proxy != null;
if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty)
{
return;
}
_logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect
foreach (var connection in _socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy);
}
}
/// <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?>
@ -120,20 +121,32 @@ 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)
{ {
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
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 (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 GetUndefinedEnumValue();
}
private T GetUndefinedEnumValue()
{
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 isEmptyString) private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)

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

@ -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

@ -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
@ -28,4 +29,20 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary> /// </summary>
bool Disposed { get; } bool Disposed { get; }
} }
/// <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
@ -60,4 +61,21 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <returns></returns> /// <returns></returns>
Task UnsubscribeAllAsync(); Task UnsubscribeAllAsync();
} }
/// <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,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

@ -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,7 +6,7 @@ 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<TApiCredentials> where TApiCredentials : ApiCredentials
{ {
/// <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
@ -15,13 +15,10 @@ namespace CryptoExchange.Net.Objects.Options
/// <summary> /// <summary>
/// Api credentials /// Api credentials
/// </summary> /// </summary>
public T? ApiCredentials { get; set; } public TApiCredentials? ApiCredentials { get; set; }
/// <summary> /// <summary>
/// Request timeout /// Request timeout
/// </summary> /// </summary>
public TimeSpan? RequestTimeout { get; set; } public TimeSpan? RequestTimeout { get; set; }
} }
/// <inheritdoc />
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
} }

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

@ -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

@ -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

@ -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": {