mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-07 10:11:10 +00:00
Compare commits
31 Commits
CryptoExch
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93034e8af8 | ||
|
|
cdd0bd83ab | ||
|
|
61d371682c | ||
|
|
8aae769e54 | ||
|
|
1b4f6926df | ||
|
|
acca3468f3 | ||
|
|
d6b680d42e | ||
|
|
24123261e5 | ||
|
|
aab30e05f0 | ||
|
|
54b15b75d3 | ||
|
|
d715c7df59 | ||
|
|
e1d33f252f | ||
|
|
b2ba9e251f | ||
|
|
2f9f97f4e8 | ||
|
|
875696a73a | ||
|
|
90c61715d0 | ||
|
|
12ca45050f | ||
|
|
eaf092a334 | ||
|
|
33c0fb26a7 | ||
|
|
dabbb5c868 | ||
|
|
204bda8622 | ||
|
|
78e3523a4f | ||
|
|
89a73747b0 | ||
|
|
02b70398b3 | ||
|
|
73fcb47b17 | ||
|
|
d41ca3459e | ||
|
|
bea2b2bd7b | ||
|
|
b29cdc41f3 | ||
|
|
0ce2e778f4 | ||
|
|
36c2411d46 | ||
|
|
6d3e72745a |
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using NUnit.Framework;
|
||||
@ -29,7 +30,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// act
|
||||
// assert
|
||||
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]
|
||||
@ -38,7 +45,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
ApiCredentials = new HMACCredential("123", "456"),
|
||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
@ -49,83 +56,65 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApiOptionsAreSet()
|
||||
public void TestSetOptionsRest()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(options => {
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
});
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "333");
|
||||
Assert.That(authProvider2.GetSecret() == "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient();
|
||||
client.SetOptions(new UpdateOptions
|
||||
{
|
||||
RequestTimeout = TimeSpan.FromSeconds(2),
|
||||
Proxy = new ApiProxy("http://testproxy", 1234)
|
||||
});
|
||||
|
||||
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;
|
||||
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
|
||||
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
|
||||
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
|
||||
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
public void TestSetOptionsRestWithCredentials()
|
||||
{
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient(options =>
|
||||
var client = new TestRestClient();
|
||||
client.SetOptions(new UpdateOptions<HMACCredential>
|
||||
{
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||
ApiCredentials = new HMACCredential("123", "456"),
|
||||
RequestTimeout = TimeSpan.FromSeconds(2),
|
||||
Proxy = new ApiProxy("http://testproxy", 1234)
|
||||
});
|
||||
|
||||
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");
|
||||
Assert.That(client.Api1.ApiCredentials, Is.Not.Null);
|
||||
Assert.That(client.Api1.ApiCredentials.Key, Is.EqualTo("123"));
|
||||
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
|
||||
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
|
||||
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
|
||||
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
||||
}
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
[Test]
|
||||
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new HMACCredential("111", "222"),
|
||||
RequestTimeout = TimeSpan.FromSeconds(1),
|
||||
};
|
||||
|
||||
var client1 = new TestRestClient();
|
||||
|
||||
Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
|
||||
Assert.That(client1.ClientOptions.ApiCredentials.Key, Is.EqualTo("111"));
|
||||
|
||||
TestClientOptions.Default.ApiCredentials = new HMACCredential("333", "444");
|
||||
TestClientOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
var client2 = new TestRestClient();
|
||||
|
||||
Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
||||
Assert.That(client2.ClientOptions.ApiCredentials.Key, Is.EqualTo("333"));
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
|
||||
@ -81,6 +81,25 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(result.Error is ServerError);
|
||||
}
|
||||
|
||||
|
||||
[TestCase]
|
||||
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
var response = "<html>...</html>";
|
||||
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is DeserializeError);
|
||||
Assert.That(result.Error.Message.Contains(response));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
{
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Testing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@ -155,15 +159,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", TestEnum.One)]
|
||||
[TestCase(null, TestEnum.One)]
|
||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
||||
[TestCase("Four1", (TestEnum)(-9))]
|
||||
[TestCase(null, (TestEnum)(-9))]
|
||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
||||
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("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
@ -178,6 +189,31 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(result == expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
|
||||
{
|
||||
LibraryHelpers.StaticLogger = new TraceLogger();
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
EnumConverter<TestEnum>.Reset();
|
||||
try
|
||||
{
|
||||
Assert.Throws<Exception>(() =>
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||
});
|
||||
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
@ -425,6 +461,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
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(Test2))]
|
||||
[JsonSerializable(typeof(Test3))]
|
||||
|
||||
@ -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();
|
||||
|
||||
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,24 +58,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
/// <inheritdoc />
|
||||
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 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();
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
public class TestAuthProvider : AuthenticationProvider<HMACCredential, HMACCredential>
|
||||
{
|
||||
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
|
||||
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
public TestAuthProvider(HMACCredential credentials) : base(credentials, credentials)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key;
|
||||
public string GetSecret() => _credentials.Secret;
|
||||
|
||||
public string GetKey() => Credential.Key;
|
||||
public string GetSecret() => Credential.Secret;
|
||||
}
|
||||
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
|
||||
@ -23,7 +23,7 @@ using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestRestClient: BaseRestClient
|
||||
public class TestRestClient: BaseRestClient<TestEnvironment, HMACCredential>
|
||||
{
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
Initialize(options.Value);
|
||||
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
Api1 = AddApiClient(new TestRestApi1Client(options.Value));
|
||||
Api2 = AddApiClient(new TestRestApi2Client(options.Value));
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@ -56,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<Encoding>(), It.IsAny<string>())).Callback(new Action<string, Encoding, string>((content, encoding, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
@ -130,7 +130,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
public class TestRestApi1Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||
|
||||
@ -159,7 +159,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
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();
|
||||
|
||||
@ -186,8 +186,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
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);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
|
||||
@ -19,11 +19,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||
|
||||
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||
{
|
||||
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
||||
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
|
||||
if (result.Item1 != null)
|
||||
return result.Item1;
|
||||
|
||||
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
||||
var errorData = result.Item2.Deserialize<TestError>();
|
||||
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,101 +1,19 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials
|
||||
public abstract class ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key / label to authenticate requests
|
||||
/// Validate the API credentials
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <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();
|
||||
}
|
||||
public abstract void Validate();
|
||||
|
||||
/// <summary>
|
||||
/// Copy the credentials
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||
}
|
||||
public abstract ApiCredentials Copy();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default;
|
||||
using System.Net;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
@ -24,58 +25,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
/// <summary>
|
||||
/// The supported credential types
|
||||
/// The public identifier for the provided credentials
|
||||
/// </summary>
|
||||
public abstract ApiCredentialsType[] SupportedCredentialTypes { 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);
|
||||
}
|
||||
public abstract string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate a REST request
|
||||
@ -276,21 +228,15 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <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);
|
||||
protected string SignHMACSHA256(HMACCredential credential, string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA256(credential,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)
|
||||
protected string SignHMACSHA256(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
using var encryptor = new HMACSHA256(credential.GetSBytes());
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
@ -298,21 +244,15 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <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);
|
||||
protected string SignHMACSHA384(HMACCredential credential, string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA384(credential, 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)
|
||||
protected string SignHMACSHA384(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
using var encryptor = new HMACSHA384(credential.GetSBytes());
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
@ -320,21 +260,15 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <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);
|
||||
protected string SignHMACSHA512(HMACCredential credential, string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA512(credential, 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)
|
||||
protected string SignHMACSHA512(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA512(_sBytes);
|
||||
using var encryptor = new HMACSHA512(credential.GetSBytes());
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
@ -342,27 +276,21 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// SHA256 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
protected string SignRSASHA256(RSACredential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
var rsa = credential.GetSigner();
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
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>
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
protected string SignRSASHA384(RSACredential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
var rsa = credential.GetSigner();
|
||||
using var sha384 = SHA384.Create();
|
||||
var hash = sha384.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||
@ -372,79 +300,32 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// SHA512 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||
protected string SignRSASHA512(RSACredential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
var rsa = credential.GetSigner();
|
||||
using var sha512 = SHA512.Create();
|
||||
var hash = sha512.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||
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 (Ed25519Key == null)
|
||||
{
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
var keyBytes = Convert.FromBase64String(key);
|
||||
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
|
||||
}
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(Ed25519Credential credential, string data, SignOutputType? outputType = null)
|
||||
=> SignEd25519(credential, Encoding.ASCII.GetBytes(data), outputType);
|
||||
|
||||
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
#else
|
||||
throw new InvalidOperationException();
|
||||
#endif
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(Ed25519Credential credential, byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#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;
|
||||
var signKey = credential.GetSigningKey();
|
||||
var resultBytes = SignatureAlgorithm.Ed25519.Sign(signKey, data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to hex string
|
||||
@ -571,17 +452,179 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider
|
||||
where TApiCredentials: ApiCredentials
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||
/// <summary>
|
||||
/// API credentials used for signing requests
|
||||
/// </summary>
|
||||
public TApiCredentials ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||
protected AuthenticationProvider(TApiCredentials 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
|
||||
}
|
||||
}
|
||||
|
||||
569
CryptoExchange.Net/Authentication/CredentialSet.cs
Normal file
569
CryptoExchange.Net/Authentication/CredentialSet.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
147
CryptoExchange.Net/Authentication/Signing/CeAbiEncoder.cs
Normal file
147
CryptoExchange.Net/Authentication/Signing/CeAbiEncoder.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication.Signing
|
||||
{
|
||||
/// <summary>
|
||||
/// ABI encoding
|
||||
/// </summary>
|
||||
public static class CeAbiEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// ABI encode string as Sha3Keccack hashed byte value
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeString(string value)
|
||||
{
|
||||
var abiValueEncoded = CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(value));
|
||||
return abiValueEncoded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode bool value as uint256 with 1 for true and 0 for false, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeBool(bool value)
|
||||
=> AbiValueEncodeInt((byte)(value ? 1 : 0));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode byte value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(byte value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode short value as int256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(short value)
|
||||
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode int value as int256, as per ABI specification
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeInt(int value)
|
||||
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode long value as int256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(long value)
|
||||
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode ushort value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(ushort value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode uint value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeInt(uint value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode ulong value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeInt(ulong value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode big integer value as int256 or uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeBigInteger(bool signed, BigInteger value)
|
||||
{
|
||||
var result = new byte[32];
|
||||
if (signed && value < 0)
|
||||
{
|
||||
// Pad with FF
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
result[i] = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
var t = value.ToByteArray();
|
||||
if (t.Length == 33)
|
||||
{
|
||||
// Strip last byte
|
||||
var strip1 = new byte[32];
|
||||
Array.Copy(t, 0, strip1, 0, 32);
|
||||
t = strip1;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
t = t.AsEnumerable().Reverse().ToArray();
|
||||
|
||||
t.CopyTo(result, result.Length - t.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode address value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeAddress(string value)
|
||||
{
|
||||
var result = new byte[32];
|
||||
var h = value.HexStringToBytes();
|
||||
h.CopyTo(result, result.Length - h.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode hex string value as bytes32, as per ABI specification. The hex string is expected to be a 0x prefixed string, and the resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the hex string is shorter than 32 bytes. If the hex string is longer than 32 bytes, an exception will be thrown.
|
||||
/// </summary>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeHexBytes(int length, string value)
|
||||
=> AbiValueEncodeBytes(value.Length, value.HexStringToBytes());
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode byte array value as bytes32, as per ABI specification. The resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the byte array is shorter than 32 bytes. If the byte array is longer than 32 bytes, an exception will be thrown.
|
||||
/// </summary>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static byte[] AbiValueEncodeBytes(int length, byte[] value)
|
||||
{
|
||||
if (length != 32)
|
||||
throw new Exception("Only 32 bytes size supported");
|
||||
|
||||
if (value.Length == 32)
|
||||
return value;
|
||||
|
||||
var result = new byte[32];
|
||||
value.CopyTo(result, result.Length - value.Length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication.Signing
|
||||
{
|
||||
/// <summary>
|
||||
/// EIP712 Typed Data Encoder
|
||||
/// </summary>
|
||||
public static class CeEip712TypedDataEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
|
||||
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
|
||||
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
|
||||
/// </summary>
|
||||
public static byte[] EncodeEip721(
|
||||
string primaryType,
|
||||
IEnumerable<(string Name, string Type, object Value)> domainFields,
|
||||
IEnumerable<(string Name, string Type, object Value)> messageFields)
|
||||
{
|
||||
var data = new CeTypedDataRaw()
|
||||
{
|
||||
PrimaryType = primaryType,
|
||||
DomainRawValues = domainFields.Select(x => new CeMemberValue
|
||||
{
|
||||
TypeName = x.Type,
|
||||
Value = x.Value,
|
||||
}).ToArray(),
|
||||
|
||||
Message = messageFields.Select(x => new CeMemberValue
|
||||
{
|
||||
TypeName = x.Type,
|
||||
Value = x.Value,
|
||||
}).ToArray(),
|
||||
Types = new Dictionary<string, CeMemberDescription[]>
|
||||
{
|
||||
{
|
||||
"EIP712Domain",
|
||||
domainFields.Select(x => new CeMemberDescription
|
||||
{
|
||||
Name = x.Name,
|
||||
Type = x.Type
|
||||
}).ToArray()
|
||||
},
|
||||
{
|
||||
primaryType,
|
||||
messageFields.Select(x => new CeMemberDescription
|
||||
{
|
||||
Name = x.Name,
|
||||
Type = x.Type
|
||||
}).ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return EncodeTypedDataRaw(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
|
||||
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
|
||||
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
|
||||
/// </summary>
|
||||
public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(memoryStream);
|
||||
|
||||
// Write 0x19 0x01 prefix
|
||||
writer.Write((byte)0x19);
|
||||
writer.Write((byte)0x01);
|
||||
|
||||
// Write domain
|
||||
writer.Write(HashStruct(typedData.Types, "EIP712Domain", typedData.DomainRawValues));
|
||||
|
||||
// Write message
|
||||
writer.Write(HashStruct(typedData.Types, typedData.PrimaryType, typedData.Message));
|
||||
|
||||
writer.Flush();
|
||||
var result = memoryStream.ToArray();
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private static byte[] HashStruct(IDictionary<string, CeMemberDescription[]> types, string primaryType, IEnumerable<CeMemberValue> message)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
var writer = new BinaryWriter(memoryStream);
|
||||
|
||||
// Encode the type header
|
||||
EncodeType(writer, types, primaryType);
|
||||
|
||||
// Encode the data
|
||||
EncodeData(writer, types, message);
|
||||
|
||||
writer.Flush();
|
||||
return CeSha3Keccack.CalculateHash(memoryStream.ToArray());
|
||||
|
||||
}
|
||||
|
||||
private static void EncodeData(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, IEnumerable<CeMemberValue> memberValues)
|
||||
{
|
||||
foreach (var memberValue in memberValues)
|
||||
{
|
||||
switch (memberValue.TypeName)
|
||||
{
|
||||
case var refType when IsReferenceType(refType):
|
||||
writer.Write(HashStruct(types, memberValue.TypeName, (IEnumerable<CeMemberValue>)memberValue.Value));
|
||||
break;
|
||||
|
||||
case "string":
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeString((string)memberValue.Value));
|
||||
break;
|
||||
|
||||
case "bool":
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBool((bool)memberValue.Value));
|
||||
break;
|
||||
|
||||
case "address":
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeAddress((string)memberValue.Value));
|
||||
break;
|
||||
|
||||
default:
|
||||
if (memberValue.TypeName.Contains("["))
|
||||
{
|
||||
var items = (IList)memberValue.Value;
|
||||
var itemsMemberValues = new List<CeMemberValue>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
itemsMemberValues.Add(new CeMemberValue()
|
||||
{
|
||||
TypeName = memberValue.TypeName.Substring(0, memberValue.TypeName.LastIndexOf("[")),
|
||||
Value = item
|
||||
});
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
var writerItem = new BinaryWriter(memoryStream);
|
||||
|
||||
EncodeData(writerItem, types, itemsMemberValues);
|
||||
writerItem.Flush();
|
||||
writer.Write(CeSha3Keccack.CalculateHash(memoryStream.ToArray()));
|
||||
}
|
||||
else if (memberValue.TypeName.StartsWith("int") || memberValue.TypeName.StartsWith("uint"))
|
||||
{
|
||||
if (memberValue.Value is string v)
|
||||
{
|
||||
if (!BigInteger.TryParse(v, out BigInteger parsedOutput))
|
||||
throw new Exception($"Failed to encode BigInteger string {v}");
|
||||
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput));
|
||||
}
|
||||
else if (memberValue.Value is byte b)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(b));
|
||||
}
|
||||
else if (memberValue.Value is short s)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(s));
|
||||
}
|
||||
else if (memberValue.Value is int i)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(i));
|
||||
}
|
||||
else if (memberValue.Value is long l)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(l));
|
||||
}
|
||||
else if (memberValue.Value is ushort us)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(us));
|
||||
}
|
||||
else if (memberValue.Value is uint ui)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ui));
|
||||
}
|
||||
else if (memberValue.Value is ulong ul)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ul));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unknown number value");
|
||||
}
|
||||
}
|
||||
else if (memberValue.TypeName.StartsWith("bytes"))
|
||||
{
|
||||
var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5));
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value));
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EncodeType(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, string typeName)
|
||||
{
|
||||
var encodedTypes = EncodeTypes(types, typeName);
|
||||
var encodedPrimaryType = encodedTypes.Single(x => x.Key == typeName);
|
||||
var encodedReferenceTypes = encodedTypes.Where(x => x.Key != typeName).OrderBy(x => x.Key).Select(x => x.Value);
|
||||
var fullyEncodedType = encodedPrimaryType.Value + string.Join(string.Empty, encodedReferenceTypes.ToArray());
|
||||
|
||||
writer.Write(CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(fullyEncodedType)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a list of type => type(parameters), for example:<br />
|
||||
/// { IP712Domain, EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) }
|
||||
/// </summary>
|
||||
private static IList<KeyValuePair<string, string>> EncodeTypes(IDictionary<string, CeMemberDescription[]> types, string currentTypeName)
|
||||
{
|
||||
var currentTypeMembers = types[currentTypeName];
|
||||
var currentTypeMembersEncoded = currentTypeMembers.Select(x => x.Type + " " + x.Name);
|
||||
var result = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(currentTypeName, currentTypeName + "(" + string.Join(",", currentTypeMembersEncoded.ToArray()) + ")")
|
||||
};
|
||||
|
||||
result.AddRange(currentTypeMembers.Select(x => x.Type.Contains("[") ? x.Type.Substring(0, x.Type.IndexOf("[")) : x.Type)
|
||||
.Distinct()
|
||||
.Where(IsReferenceType)
|
||||
.SelectMany(x => EncodeTypes(types, x)));
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool IsReferenceType(string typeName)
|
||||
{
|
||||
switch (typeName)
|
||||
{
|
||||
case var bytes when new Regex("bytes\\d+").IsMatch(bytes):
|
||||
case var @uint when new Regex("uint\\d+").IsMatch(@uint):
|
||||
case var @int when new Regex("int\\d+").IsMatch(@int):
|
||||
case "bytes":
|
||||
case "string":
|
||||
case "bool":
|
||||
case "address":
|
||||
case var array when array.Contains("["):
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Member description
|
||||
/// </summary>
|
||||
public class CeMemberDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Type
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Member value
|
||||
/// </summary>
|
||||
public class CeMemberValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Type name
|
||||
/// </summary>
|
||||
public string TypeName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Value
|
||||
/// </summary>
|
||||
public object Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed data raw, used for encoding EIP712 typed data with the provided primary type, domain fields and message fields.
|
||||
/// </summary>
|
||||
public class CeTypedDataRaw
|
||||
{
|
||||
/// <summary>
|
||||
/// Type dictionary
|
||||
/// </summary>
|
||||
public IDictionary<string, CeMemberDescription[]> Types { get; set; } = new Dictionary<string, CeMemberDescription[]>();
|
||||
/// <summary>
|
||||
/// Primary type
|
||||
/// </summary>
|
||||
public string PrimaryType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Message values
|
||||
/// </summary>
|
||||
public CeMemberValue[] Message { get; set; } = [];
|
||||
/// <summary>
|
||||
/// Domain values
|
||||
/// </summary>
|
||||
public CeMemberValue[] DomainRawValues { get; set; } = [];
|
||||
}
|
||||
}
|
||||
385
CryptoExchange.Net/Authentication/Signing/CeSha3Keccack.cs
Normal file
385
CryptoExchange.Net/Authentication/Signing/CeSha3Keccack.cs
Normal file
@ -0,0 +1,385 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication.Signing
|
||||
{
|
||||
/// <summary>
|
||||
/// Sha3 Keccack hashing, as per Ethereum specification, with 256 bit output
|
||||
/// </summary>
|
||||
public class CeSha3Keccack
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculate the Keccack256 hash of the provided data, as per Ethereum specification
|
||||
/// </summary>
|
||||
public static byte[] CalculateHash(byte[] data)
|
||||
{
|
||||
var digest = new CeKeccakDigest256();
|
||||
var output = new byte[digest.GetDigestSize()];
|
||||
digest.BlockUpdate(data, data.Length);
|
||||
digest.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
internal class CeKeccakDigest256
|
||||
{
|
||||
private static readonly ulong[] _keccakRoundConstants = KeccakInitializeRoundConstants();
|
||||
private static readonly int[] _keccakRhoOffsets = KeccakInitializeRhoOffsets();
|
||||
|
||||
private readonly int _rate;
|
||||
private const int _stateLength = 1600 / 8;
|
||||
private readonly ulong[] _state = new ulong[_stateLength / 8];
|
||||
private readonly byte[] _dataQueue = new byte[1536 / 8];
|
||||
private int _bitsInQueue;
|
||||
private int _fixedOutputLength;
|
||||
private bool _squeezing;
|
||||
private int _bitsAvailableForSqueezing;
|
||||
|
||||
public CeKeccakDigest256()
|
||||
{
|
||||
_rate = 1600 - (256 << 1);
|
||||
_bitsInQueue = 0;
|
||||
_squeezing = false;
|
||||
_bitsAvailableForSqueezing = 0;
|
||||
_fixedOutputLength = 1600 - _rate >> 1;
|
||||
}
|
||||
|
||||
internal void BlockUpdate(byte[] data, int length)
|
||||
{
|
||||
int bytesInQueue = _bitsInQueue >> 3;
|
||||
int rateBytes = _rate >> 3;
|
||||
|
||||
int count = 0;
|
||||
while (count < length)
|
||||
{
|
||||
if (bytesInQueue == 0 && count <= length - rateBytes)
|
||||
{
|
||||
do
|
||||
{
|
||||
KeccakAbsorb(data, count);
|
||||
count += rateBytes;
|
||||
} while (count <= length - rateBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
int partialBlock = Math.Min(rateBytes - bytesInQueue, length - count);
|
||||
Array.Copy(data, count, _dataQueue, bytesInQueue, partialBlock);
|
||||
|
||||
bytesInQueue += partialBlock;
|
||||
count += partialBlock;
|
||||
|
||||
if (bytesInQueue == rateBytes)
|
||||
{
|
||||
KeccakAbsorb(_dataQueue, 0);
|
||||
bytesInQueue = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bitsInQueue = bytesInQueue << 3;
|
||||
}
|
||||
|
||||
internal void DoFinal(byte[] output, int outOff)
|
||||
{
|
||||
Squeeze(output, outOff, _fixedOutputLength >> 3);
|
||||
}
|
||||
|
||||
internal int GetDigestSize() => _fixedOutputLength >> 3;
|
||||
|
||||
protected void Squeeze(byte[] output, int off, int len)
|
||||
{
|
||||
if (!_squeezing)
|
||||
PadAndSwitchToSqueezingPhase();
|
||||
|
||||
long outputLength = (long)len << 3;
|
||||
long i = 0;
|
||||
while (i < outputLength)
|
||||
{
|
||||
if (_bitsAvailableForSqueezing == 0)
|
||||
{
|
||||
KeccakPermutation();
|
||||
KeccakExtract();
|
||||
_bitsAvailableForSqueezing = _rate;
|
||||
}
|
||||
|
||||
int partialBlock = (int)Math.Min(_bitsAvailableForSqueezing, outputLength - i);
|
||||
Array.Copy(_dataQueue, _rate - _bitsAvailableForSqueezing >> 3, output, off + (int)(i >> 3),
|
||||
partialBlock >> 3);
|
||||
_bitsAvailableForSqueezing -= partialBlock;
|
||||
i += partialBlock;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong[] KeccakInitializeRoundConstants()
|
||||
{
|
||||
ulong[] keccakRoundConstants = new ulong[24];
|
||||
byte LFSRState = 0x01;
|
||||
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
keccakRoundConstants[i] = 0;
|
||||
for (int j = 0; j < 7; j++)
|
||||
{
|
||||
int bitPosition = (1 << j) - 1;
|
||||
|
||||
// LFSR86540
|
||||
|
||||
bool loBit = (LFSRState & 0x01) != 0;
|
||||
if (loBit)
|
||||
keccakRoundConstants[i] ^= 1UL << bitPosition;
|
||||
|
||||
bool hiBit = (LFSRState & 0x80) != 0;
|
||||
LFSRState <<= 1;
|
||||
if (hiBit)
|
||||
LFSRState ^= 0x71;
|
||||
}
|
||||
}
|
||||
|
||||
return keccakRoundConstants;
|
||||
}
|
||||
private static int[] KeccakInitializeRhoOffsets()
|
||||
{
|
||||
int[] keccakRhoOffsets = new int[25];
|
||||
int x, y, t, newX, newY;
|
||||
|
||||
int rhoOffset = 0;
|
||||
keccakRhoOffsets[0] = rhoOffset;
|
||||
x = 1;
|
||||
y = 0;
|
||||
for (t = 1; t < 25; t++)
|
||||
{
|
||||
rhoOffset = rhoOffset + t & 63;
|
||||
keccakRhoOffsets[x % 5 + 5 * (y % 5)] = rhoOffset;
|
||||
newX = (0 * x + 1 * y) % 5;
|
||||
newY = (2 * x + 3 * y) % 5;
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
|
||||
return keccakRhoOffsets;
|
||||
}
|
||||
|
||||
private void KeccakAbsorb(byte[] data, int off)
|
||||
{
|
||||
int count = _rate >> 6;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
_state[i] ^= Pack.LeToUInt64(data, off);
|
||||
off += 8;
|
||||
}
|
||||
|
||||
KeccakPermutation();
|
||||
}
|
||||
|
||||
private void KeccakPermutation()
|
||||
{
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
Theta(_state);
|
||||
Rho(_state);
|
||||
Pi(_state);
|
||||
Chi(_state);
|
||||
Iota(_state, i);
|
||||
}
|
||||
}
|
||||
private static ulong LeftRotate(ulong v, int r)
|
||||
{
|
||||
return v << r | v >> -r;
|
||||
}
|
||||
|
||||
private static void Theta(ulong[] A)
|
||||
{
|
||||
ulong C0 = A[0 + 0] ^ A[0 + 5] ^ A[0 + 10] ^ A[0 + 15] ^ A[0 + 20];
|
||||
ulong C1 = A[1 + 0] ^ A[1 + 5] ^ A[1 + 10] ^ A[1 + 15] ^ A[1 + 20];
|
||||
ulong C2 = A[2 + 0] ^ A[2 + 5] ^ A[2 + 10] ^ A[2 + 15] ^ A[2 + 20];
|
||||
ulong C3 = A[3 + 0] ^ A[3 + 5] ^ A[3 + 10] ^ A[3 + 15] ^ A[3 + 20];
|
||||
ulong C4 = A[4 + 0] ^ A[4 + 5] ^ A[4 + 10] ^ A[4 + 15] ^ A[4 + 20];
|
||||
|
||||
ulong dX = LeftRotate(C1, 1) ^ C4;
|
||||
|
||||
A[0] ^= dX;
|
||||
A[5] ^= dX;
|
||||
A[10] ^= dX;
|
||||
A[15] ^= dX;
|
||||
A[20] ^= dX;
|
||||
|
||||
dX = LeftRotate(C2, 1) ^ C0;
|
||||
|
||||
A[1] ^= dX;
|
||||
A[6] ^= dX;
|
||||
A[11] ^= dX;
|
||||
A[16] ^= dX;
|
||||
A[21] ^= dX;
|
||||
|
||||
dX = LeftRotate(C3, 1) ^ C1;
|
||||
|
||||
A[2] ^= dX;
|
||||
A[7] ^= dX;
|
||||
A[12] ^= dX;
|
||||
A[17] ^= dX;
|
||||
A[22] ^= dX;
|
||||
|
||||
dX = LeftRotate(C4, 1) ^ C2;
|
||||
|
||||
A[3] ^= dX;
|
||||
A[8] ^= dX;
|
||||
A[13] ^= dX;
|
||||
A[18] ^= dX;
|
||||
A[23] ^= dX;
|
||||
|
||||
dX = LeftRotate(C0, 1) ^ C3;
|
||||
|
||||
A[4] ^= dX;
|
||||
A[9] ^= dX;
|
||||
A[14] ^= dX;
|
||||
A[19] ^= dX;
|
||||
A[24] ^= dX;
|
||||
}
|
||||
|
||||
private static void Rho(ulong[] A)
|
||||
{
|
||||
// KeccakRhoOffsets[0] == 0
|
||||
for (int x = 1; x < 25; x++)
|
||||
{
|
||||
A[x] = LeftRotate(A[x], _keccakRhoOffsets[x]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Pi(ulong[] A)
|
||||
{
|
||||
ulong a1 = A[1];
|
||||
A[1] = A[6];
|
||||
A[6] = A[9];
|
||||
A[9] = A[22];
|
||||
A[22] = A[14];
|
||||
A[14] = A[20];
|
||||
A[20] = A[2];
|
||||
A[2] = A[12];
|
||||
A[12] = A[13];
|
||||
A[13] = A[19];
|
||||
A[19] = A[23];
|
||||
A[23] = A[15];
|
||||
A[15] = A[4];
|
||||
A[4] = A[24];
|
||||
A[24] = A[21];
|
||||
A[21] = A[8];
|
||||
A[8] = A[16];
|
||||
A[16] = A[5];
|
||||
A[5] = A[3];
|
||||
A[3] = A[18];
|
||||
A[18] = A[17];
|
||||
A[17] = A[11];
|
||||
A[11] = A[7];
|
||||
A[7] = A[10];
|
||||
A[10] = a1;
|
||||
}
|
||||
|
||||
private static void Chi(ulong[] A)
|
||||
{
|
||||
ulong chiC0, chiC1, chiC2, chiC3, chiC4;
|
||||
|
||||
for (int yBy5 = 0; yBy5 < 25; yBy5 += 5)
|
||||
{
|
||||
chiC0 = A[0 + yBy5] ^ ~A[(0 + 1) % 5 + yBy5] & A[(0 + 2) % 5 + yBy5];
|
||||
chiC1 = A[1 + yBy5] ^ ~A[(1 + 1) % 5 + yBy5] & A[(1 + 2) % 5 + yBy5];
|
||||
chiC2 = A[2 + yBy5] ^ ~A[(2 + 1) % 5 + yBy5] & A[(2 + 2) % 5 + yBy5];
|
||||
chiC3 = A[3 + yBy5] ^ ~A[(3 + 1) % 5 + yBy5] & A[(3 + 2) % 5 + yBy5];
|
||||
chiC4 = A[4 + yBy5] ^ ~A[(4 + 1) % 5 + yBy5] & A[(4 + 2) % 5 + yBy5];
|
||||
|
||||
A[0 + yBy5] = chiC0;
|
||||
A[1 + yBy5] = chiC1;
|
||||
A[2 + yBy5] = chiC2;
|
||||
A[3 + yBy5] = chiC3;
|
||||
A[4 + yBy5] = chiC4;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Iota(ulong[] A, int indexRound)
|
||||
{
|
||||
A[0] ^= _keccakRoundConstants[indexRound];
|
||||
}
|
||||
|
||||
private void PadAndSwitchToSqueezingPhase()
|
||||
{
|
||||
Debug.Assert(_bitsInQueue < _rate);
|
||||
|
||||
_dataQueue[_bitsInQueue >> 3] |= (byte)(1U << (_bitsInQueue & 7));
|
||||
|
||||
if (++_bitsInQueue == _rate)
|
||||
{
|
||||
KeccakAbsorb(_dataQueue, 0);
|
||||
_bitsInQueue = 0;
|
||||
}
|
||||
|
||||
{
|
||||
int full = _bitsInQueue >> 6, partial = _bitsInQueue & 63;
|
||||
int off = 0;
|
||||
for (int i = 0; i < full; ++i)
|
||||
{
|
||||
_state[i] ^= Pack.LeToUInt64(_dataQueue, off);
|
||||
off += 8;
|
||||
}
|
||||
|
||||
if (partial > 0)
|
||||
{
|
||||
ulong mask = (1UL << partial) - 1UL;
|
||||
_state[full] ^= Pack.LeToUInt64(_dataQueue, off) & mask;
|
||||
}
|
||||
|
||||
_state[_rate - 1 >> 6] ^= 1UL << 63;
|
||||
}
|
||||
|
||||
KeccakPermutation();
|
||||
KeccakExtract();
|
||||
_bitsAvailableForSqueezing = _rate;
|
||||
|
||||
_bitsInQueue = 0;
|
||||
_squeezing = true;
|
||||
}
|
||||
private void KeccakExtract()
|
||||
{
|
||||
Pack.UInt64ToLe(_state, 0, _rate >> 6, _dataQueue, 0);
|
||||
}
|
||||
|
||||
static class Pack
|
||||
{
|
||||
internal static ulong LeToUInt64(byte[] bs, int off)
|
||||
{
|
||||
uint lo = LeToUInt32(bs, off);
|
||||
uint hi = LeToUInt32(bs, off + 4);
|
||||
return (ulong)hi << 32 | lo;
|
||||
}
|
||||
internal static uint LeToUInt32(byte[] bs, int off)
|
||||
{
|
||||
return bs[off]
|
||||
| (uint)bs[off + 1] << 8
|
||||
| (uint)bs[off + 2] << 16
|
||||
| (uint)bs[off + 3] << 24;
|
||||
}
|
||||
|
||||
internal static void UInt64ToLe(ulong[] ns, int nsOff, int nsLen, byte[] bs, int bsOff)
|
||||
{
|
||||
for (int i = 0; i < nsLen; ++i)
|
||||
{
|
||||
UInt64ToLe(ns[nsOff + i], bs, bsOff);
|
||||
bsOff += 8;
|
||||
}
|
||||
}
|
||||
internal static void UInt64ToLe(ulong n, byte[] bs, int off)
|
||||
{
|
||||
UInt32ToLe((uint)n, bs, off);
|
||||
UInt32ToLe((uint)(n >> 32), bs, off + 4);
|
||||
}
|
||||
|
||||
internal static void UInt32ToLe(uint n, byte[] bs, int off)
|
||||
{
|
||||
bs[off] = (byte)n;
|
||||
bs[off + 1] = (byte)(n >> 8);
|
||||
bs[off + 2] = (byte)(n >> 16);
|
||||
bs[off + 3] = (byte)(n >> 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
@ -28,6 +27,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a proxy is configured
|
||||
/// </summary>
|
||||
protected bool _proxyConfigured;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the client
|
||||
/// </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>
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
@ -58,12 +57,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
@ -85,10 +78,14 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="logger">Logger</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="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client 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;
|
||||
|
||||
@ -96,19 +93,10 @@ namespace CryptoExchange.Net.Clients
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
ApiCredentials = apiCredentials?.Copy();
|
||||
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
_proxyConfigured = ClientOptions.Proxy != null;
|
||||
}
|
||||
|
||||
/// <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 />
|
||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
@ -122,25 +110,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
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>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@ -92,16 +92,6 @@ namespace CryptoExchange.Net.Clients
|
||||
_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>
|
||||
/// Register an API client
|
||||
/// </summary>
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
@ -10,6 +14,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||
|
||||
@ -24,5 +33,59 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update options
|
||||
/// </summary>
|
||||
public virtual void SetOptions(UpdateOptions options)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetOptions(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class BaseRestClient<TEnvironment, TApiCredentials> : BaseRestClient, IRestClient<TApiCredentials>
|
||||
where TEnvironment : TradeEnvironment
|
||||
where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
public void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update options
|
||||
/// </summary>
|
||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetOptions(options);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -19,6 +21,11 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
#region fields
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
@ -133,5 +140,58 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update options
|
||||
/// </summary>
|
||||
public virtual void SetOptions(UpdateOptions options)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class BaseSocketClient<TEnvironment, TApiCredentials> : BaseSocketClient, ISocketClient<TApiCredentials>
|
||||
where TEnvironment : TradeEnvironment
|
||||
where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update options
|
||||
/// </summary>
|
||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetOptions(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Caching;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
@ -77,6 +78,16 @@ namespace CryptoExchange.Net.Clients
|
||||
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Encoding/charset for the ContentType header
|
||||
/// </summary>
|
||||
protected Encoding? RequestBodyContentEncoding { get; set; } = Encoding.UTF8;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to omit the ContentType header if there is no content
|
||||
/// </summary>
|
||||
protected bool OmitContentTypeHeaderWithoutContent { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||
|
||||
@ -92,7 +103,17 @@ namespace CryptoExchange.Net.Clients
|
||||
/// The message handler
|
||||
/// </summary>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@ -101,10 +122,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base 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,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
@ -204,7 +228,7 @@ namespace CryptoExchange.Net.Clients
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
if (definition.Authenticated && AuthenticationProvider == null)
|
||||
if (definition.Authenticated && GetAuthenticationProvider() == null)
|
||||
{
|
||||
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
||||
@ -309,7 +333,17 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
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)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
@ -324,7 +358,18 @@ namespace CryptoExchange.Net.Clients
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
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)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
@ -363,7 +408,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
try
|
||||
{
|
||||
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
|
||||
GetAuthenticationProvider()?.ProcessRequest(this, requestConfiguration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -374,7 +419,11 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||
queryString = $"?{queryString}";
|
||||
|
||||
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||
var path = baseAddress.AppendPath(definition.Path);
|
||||
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
|
||||
path += "/";
|
||||
|
||||
var uri = new Uri(path + queryString);
|
||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||
request.Accept = MessageHandler.AcceptHeader;
|
||||
|
||||
@ -398,14 +447,14 @@ namespace CryptoExchange.Net.Clients
|
||||
var bodyContent = requestConfiguration.GetBodyContent();
|
||||
if (bodyContent != null)
|
||||
{
|
||||
request.SetContent(bodyContent, contentType);
|
||||
request.SetContent(bodyContent, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
||||
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||
else if (OmitContentTypeHeaderWithoutContent != true)
|
||||
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -437,23 +486,19 @@ namespace CryptoExchange.Net.Clients
|
||||
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
string? originalData = null;
|
||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
|
||||
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
|
||||
{
|
||||
// If we want to return the original string data from the stream, but still want to process it
|
||||
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
|
||||
var memoryStream = new MemoryStream();
|
||||
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
|
||||
if (outputOriginalData)
|
||||
// Create a seekable stream from the response stream if:
|
||||
// 1. We need to output the original data
|
||||
// 2. The message handler requires a seekable stream
|
||||
// 3. The response indicates error and we want to output (part of) the returned data
|
||||
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
|
||||
if (outputOriginalData)
|
||||
{
|
||||
memoryStream.Position = 0;
|
||||
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
responseStream.Position = 0;
|
||||
}
|
||||
|
||||
// Continue processing from the memory stream since the response stream is already read and we can't seek it
|
||||
responseStream.Close();
|
||||
memoryStream.Position = 0;
|
||||
responseStream = memoryStream;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||
@ -479,13 +524,12 @@ namespace CryptoExchange.Net.Clients
|
||||
else
|
||||
{
|
||||
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
||||
|
||||
try
|
||||
{
|
||||
error = await MessageHandler.ParseErrorResponse(
|
||||
(int)response.StatusCode,
|
||||
response.ResponseHeaders,
|
||||
responseStream).ConfigureAwait(false);
|
||||
(int)response.StatusCode,
|
||||
response.ResponseHeaders,
|
||||
responseStream).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -651,13 +695,13 @@ namespace CryptoExchange.Net.Clients
|
||||
stringData = stringSerializer.Serialize(value);
|
||||
else
|
||||
stringData = stringSerializer.Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -680,14 +724,6 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns>Server time</returns>
|
||||
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)
|
||||
{
|
||||
if (!definition.Authenticated)
|
||||
@ -769,10 +805,184 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Stream> CopyStreamAsync(Stream responseStream)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
memoryStream.Position = 0;
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
private bool ShouldCache(RequestDefinition definition)
|
||||
=> ClientOptions.CachingEnabled
|
||||
&& definition.Method == HttpMethod.Get
|
||||
&& !definition.PreventCaching;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions(UpdateOptions options)
|
||||
{
|
||||
_proxyConfigured = options.Proxy != null;
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class RestApiClient<TEnvironment> : RestApiClient, IRestApiClient
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions<TEnvironment> ClientOptions => (RestExchangeOptions<TEnvironment>)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string EnvironmentName => ClientOptions.Environment.Name;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected RestApiClient(
|
||||
ILogger logger,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions options,
|
||||
RestApiOptions apiOptions) : base(
|
||||
logger,
|
||||
httpClient,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class RestApiClient<TEnvironment, TApiCredentials> : RestApiClient<TEnvironment>, IRestApiClient<TApiCredentials>
|
||||
where TApiCredentials : ApiCredentials
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected RestApiClient(
|
||||
ILogger logger,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
RestApiOptions apiOptions) : base(
|
||||
logger,
|
||||
httpClient,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
ApiCredentials = options.ApiCredentials;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
ApiCredentials = (TApiCredentials)credentials.Copy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
base.SetOptions(options);
|
||||
|
||||
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class RestApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : RestApiClient<TEnvironment, TApiCredentials>
|
||||
where TApiCredentials : ApiCredentials
|
||||
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
|
||||
private bool _authProviderInitialized = false;
|
||||
private TAuthenticationProvider? _authenticationProvider;
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public TAuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_authProviderInitialized)
|
||||
{
|
||||
if (ApiCredentials != null)
|
||||
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
|
||||
_authProviderInitialized = true;
|
||||
}
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
internal set => _authenticationProvider = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected RestApiClient(
|
||||
ILogger logger,
|
||||
HttpClient? httpClient,
|
||||
string baseAddress,
|
||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
RestApiOptions apiOptions) : base(
|
||||
logger,
|
||||
httpClient,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
base.SetApiCredentials(credentials);
|
||||
|
||||
AuthenticationProvider = null;
|
||||
_authProviderInitialized = false;
|
||||
ApiCredentials = credentials;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
base.SetOptions(options);
|
||||
|
||||
if (options.ApiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider = null;
|
||||
_authProviderInitialized = false;
|
||||
ApiCredentials = options.ApiCredentials;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
@ -19,6 +20,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@ -141,6 +143,16 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
@ -150,10 +162,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="baseAddress">Base address for this API client</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,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
@ -235,7 +250,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (_disposing)
|
||||
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");
|
||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||
@ -515,7 +530,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||
{
|
||||
if (AuthenticationProvider == null)
|
||||
if (GetAuthenticationProvider() == null)
|
||||
return new CallResult(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
@ -546,7 +561,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
|
||||
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
@ -912,25 +927,6 @@ namespace CryptoExchange.Net.Clients
|
||||
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>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
@ -1039,5 +1035,174 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions(UpdateOptions options)
|
||||
{
|
||||
var previousProxyIsSet = _proxyConfigured;
|
||||
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
_proxyConfigured = options.Proxy != null;
|
||||
if ((!previousProxyIsSet && options.Proxy == null)
|
||||
|| _socketConnections.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
||||
|
||||
// Update proxy, also triggers reconnect
|
||||
foreach (var connection in _socketConnections)
|
||||
_ = connection.Value.UpdateProxy(options.Proxy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class SocketApiClient<TEnvironment> : SocketApiClient, ISocketApiClient
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions<TEnvironment> ClientOptions => (SocketExchangeOptions<TEnvironment>)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string EnvironmentName => ClientOptions.Environment.Name;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected SocketApiClient(
|
||||
ILogger logger,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions<TEnvironment> options,
|
||||
SocketApiOptions apiOptions) : base(
|
||||
logger,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class SocketApiClient<TEnvironment, TApiCredentials> : SocketApiClient<TEnvironment>, ISocketApiClient<TApiCredentials>
|
||||
where TApiCredentials : ApiCredentials
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected SocketApiClient(
|
||||
ILogger logger,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
SocketApiOptions apiOptions) : base(
|
||||
logger,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
ApiCredentials = options.ApiCredentials;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
ApiCredentials = (TApiCredentials)credentials.Copy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
base.SetOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class SocketApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : SocketApiClient<TEnvironment, TApiCredentials>
|
||||
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
||||
where TApiCredentials : ApiCredentials
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
|
||||
private bool _authProviderInitialized = false;
|
||||
private TAuthenticationProvider? _authenticationProvider;
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public TAuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_authProviderInitialized)
|
||||
{
|
||||
if (ApiCredentials != null)
|
||||
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
|
||||
_authProviderInitialized = true;
|
||||
}
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
internal set => _authenticationProvider = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected SocketApiClient(
|
||||
ILogger logger,
|
||||
string baseAddress,
|
||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
||||
SocketApiOptions apiOptions) : base(
|
||||
logger,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetApiCredentials(TApiCredentials credentials)
|
||||
{
|
||||
AuthenticationProvider = null;
|
||||
_authProviderInitialized = false;
|
||||
ApiCredentials = credentials;
|
||||
|
||||
base.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetOptions(UpdateOptions<TApiCredentials> options)
|
||||
{
|
||||
if (options.ApiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider = null;
|
||||
_authProviderInitialized = false;
|
||||
ApiCredentials = options.ApiCredentials;
|
||||
}
|
||||
|
||||
base.SetOptions(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||
|
||||
private static T? _undefinedEnumValue;
|
||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||
|
||||
internal class NullableEnumConverter : JsonConverter<T?>
|
||||
@ -119,26 +120,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
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
|
||||
{
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyStringOrNull);
|
||||
if (t != null)
|
||||
return t.Value;
|
||||
|
||||
if (isEmptyStringOrNull && !_unknownValuesWarned.Contains(null))
|
||||
{
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
_unknownValuesWarned.Add(null!);
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
|
||||
return GetUndefinedEnumValue();
|
||||
}
|
||||
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||
private T GetUndefinedEnumValue()
|
||||
{
|
||||
isEmptyString = false;
|
||||
if (_undefinedEnumValue != null)
|
||||
return _undefinedEnumValue.Value;
|
||||
|
||||
var type = typeof(T);
|
||||
if (!Enum.IsDefined(type, -9))
|
||||
_undefinedEnumValue = (T)Enum.ToObject(type, -9);
|
||||
else if (!Enum.IsDefined(type, -99))
|
||||
_undefinedEnumValue = (T)Enum.ToObject(type, -99);
|
||||
else
|
||||
_undefinedEnumValue = (T)Enum.ToObject(type, -999);
|
||||
|
||||
return (T)_undefinedEnumValue;
|
||||
}
|
||||
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
|
||||
{
|
||||
isEmptyStringOrNull = false;
|
||||
var enumType = typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
@ -154,13 +168,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
};
|
||||
|
||||
if (stringValue is null)
|
||||
{
|
||||
isEmptyStringOrNull = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!GetValue(enumType, stringValue, out var result))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
isEmptyString = true;
|
||||
isEmptyStringOrNull = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -292,6 +309,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static void Reset()
|
||||
{
|
||||
_undefinedEnumValue = null;
|
||||
_unknownValuesWarned = new ConcurrentBag<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
|
||||
@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||
{
|
||||
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||
private const int _errorResponseSnippetLimit = 128;
|
||||
|
||||
/// <summary>
|
||||
/// Empty rate limit error
|
||||
@ -80,7 +81,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
||||
var errorMsg = "Deserialization failed, invalid JSON";
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
var dataSnippet = new char[_errorResponseSnippetLimit];
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
|
||||
var data = new string(dataSnippet, 0, written);
|
||||
errorMsg += $": {data}";
|
||||
if (data.Length == _errorResponseSnippetLimit)
|
||||
errorMsg += " (truncated)";
|
||||
}
|
||||
|
||||
var error = new DeserializeError(errorMsg, ex);
|
||||
return (error, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -165,6 +165,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return type identifier for non-json messages
|
||||
/// </summary>
|
||||
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
@ -173,6 +181,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
int? arrayIndex = null;
|
||||
|
||||
_searchResult.Clear();
|
||||
if (data[0] != 0x5B && data[0] != 0x7B)
|
||||
{
|
||||
// Message doesn't start with `{` or `[`, not valid for processing as json
|
||||
return GetTypeIdentifierNonJson(data, webSocketMessageType);
|
||||
}
|
||||
|
||||
var reader = new Utf8JsonReader(data);
|
||||
while (reader.Read())
|
||||
{
|
||||
|
||||
@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("");
|
||||
throw new Exception("Invalid JSON structure");
|
||||
|
||||
reader.Read(); // Start array
|
||||
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndArray)
|
||||
throw new Exception("");
|
||||
throw new Exception("Invalid JSON structure");
|
||||
|
||||
reader.Read(); // End array
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("");
|
||||
throw new Exception("Invalid JSON structure");
|
||||
|
||||
reader.Read(); // Start array
|
||||
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
||||
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndArray)
|
||||
throw new Exception("");
|
||||
throw new Exception("Invalid JSON structure");
|
||||
|
||||
reader.Read(); // End array
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>10.6.2</PackageVersion>
|
||||
<AssemblyVersion>10.6.2</AssemblyVersion>
|
||||
<FileVersion>10.6.2</FileVersion>
|
||||
<PackageVersion>11.0.3</PackageVersion>
|
||||
<AssemblyVersion>11.0.3</AssemblyVersion>
|
||||
<FileVersion>11.0.3</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@ -4,6 +4,7 @@ using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
@ -310,11 +311,11 @@ namespace CryptoExchange.Net
|
||||
/// <param name="request">The request parameters</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<T>();
|
||||
ExchangeWebResult<T[]> batch;
|
||||
INextPageToken? nextPageToken = null;
|
||||
PageRequest? nextPageToken = null;
|
||||
while (true)
|
||||
{
|
||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||
@ -323,12 +324,42 @@ namespace CryptoExchange.Net
|
||||
break;
|
||||
|
||||
result.AddRange(batch.Data);
|
||||
nextPageToken = batch.NextPageToken;
|
||||
nextPageToken = batch.NextPageRequest;
|
||||
if (nextPageToken == null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply filters to the data set
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type</typeparam>
|
||||
/// <param name="data">Data set</param>
|
||||
/// <param name="timeSelector">Time selector for the data</param>
|
||||
/// <param name="startTime">Start time filter</param>
|
||||
/// <param name="endTime">End time filter</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
public static IEnumerable<T> ApplyFilter<T>(
|
||||
IEnumerable<T> data,
|
||||
Func<T, DateTime> timeSelector,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
DataDirection direction)
|
||||
{
|
||||
if (direction == DataDirection.Ascending)
|
||||
data = data.OrderBy(timeSelector);
|
||||
else
|
||||
data = data.OrderByDescending(timeSelector);
|
||||
|
||||
if (startTime != null)
|
||||
data = data.Where(x => timeSelector(x) >= startTime.Value);
|
||||
|
||||
if (endTime != null)
|
||||
data = data.Where(x => timeSelector(x) < endTime.Value);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
||||
/// </summary>
|
||||
@ -500,5 +531,50 @@ namespace CryptoExchange.Net
|
||||
// Unknown decimal format, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ namespace CryptoExchange.Net
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
bool firstArrayValue = true;
|
||||
foreach (var entry in (object[])parameter.Value)
|
||||
foreach (var entry in (Array)parameter.Value)
|
||||
{
|
||||
if (!firstArrayValue)
|
||||
uriString.Append('&');
|
||||
@ -92,7 +92,7 @@ namespace CryptoExchange.Net
|
||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||
{
|
||||
bool firstArrayValue = true;
|
||||
foreach (var entry in (object[])parameter.Value)
|
||||
foreach (var entry in (Array)parameter.Value)
|
||||
{
|
||||
if (!firstArrayValue)
|
||||
uriString.Append('&');
|
||||
@ -109,7 +109,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
uriString.Append('[');
|
||||
var firstArrayEntry = true;
|
||||
foreach (var entry in (object[])parameter.Value)
|
||||
foreach (var entry in (Array)parameter.Value)
|
||||
{
|
||||
if (!firstArrayEntry)
|
||||
uriString.Append(',');
|
||||
|
||||
@ -15,11 +15,6 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// </summary>
|
||||
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>
|
||||
/// Format a base and quote asset to an exchange accepted symbol
|
||||
/// </summary>
|
||||
@ -29,19 +24,5 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client
|
||||
@ -15,4 +18,25 @@
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
@ -6,7 +7,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// <summary>
|
||||
/// Base class for rest API implementations
|
||||
/// </summary>
|
||||
public interface IRestClient: IDisposable
|
||||
public interface IRestClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The options provided for this client
|
||||
@ -27,5 +28,27 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// Whether client is disposed
|
||||
/// </summary>
|
||||
bool Disposed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Update specific options
|
||||
/// </summary>
|
||||
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
||||
void SetOptions(UpdateOptions options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||
@ -10,7 +11,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// <summary>
|
||||
/// Socket API client
|
||||
/// </summary>
|
||||
public interface ISocketApiClient: IBaseApiClient
|
||||
public interface ISocketApiClient : IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The current amount of socket connections on the API client
|
||||
@ -73,4 +74,25 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
|
||||
@ -8,7 +9,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// <summary>
|
||||
/// Base class for socket API implementations
|
||||
/// </summary>
|
||||
public interface ISocketClient: IDisposable
|
||||
public interface ISocketClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The exchange name
|
||||
@ -59,5 +60,28 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAllAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Update specific options
|
||||
/// </summary>
|
||||
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
||||
void SetOptions(UpdateOptions options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -43,9 +44,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Set string content
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="contentType"></param>
|
||||
void SetContent(string data, string contentType);
|
||||
void SetContent(string data, Encoding? encoding, string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Add a header to the request
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -19,7 +21,7 @@ namespace CryptoExchange.Net
|
||||
public static ILogger? StaticLogger
|
||||
{
|
||||
get => _staticLogger;
|
||||
internal set
|
||||
internal set
|
||||
{
|
||||
if (_staticLogger != null)
|
||||
return;
|
||||
@ -105,31 +107,36 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Create a new HttpMessageHandler instance
|
||||
/// </summary>
|
||||
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
|
||||
public static HttpMessageHandler CreateHttpClientMessageHandler(RestExchangeOptions options)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
var socketHandler = new SocketsHttpHandler();
|
||||
try
|
||||
{
|
||||
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
|
||||
if (options.HttpKeepAliveInterval != null && options.HttpKeepAliveInterval != TimeSpan.Zero)
|
||||
{
|
||||
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
||||
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
|
||||
socketHandler.KeepAlivePingDelay = options.HttpKeepAliveInterval.Value;
|
||||
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
|
||||
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||
|
||||
socketHandler.EnableMultipleHttp2Connections = options.HttpEnableMultipleHttp2Connections;
|
||||
socketHandler.PooledConnectionLifetime = options.HttpPooledConnectionLifetime;
|
||||
socketHandler.PooledConnectionIdleTimeout = options.HttpPooledConnectionIdleTimeout;
|
||||
socketHandler.MaxConnectionsPerServer = options.HttpMaxConnectionsPerServer;
|
||||
}
|
||||
catch (PlatformNotSupportedException) { }
|
||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||
|
||||
if (proxy != null)
|
||||
if (options.Proxy != null)
|
||||
{
|
||||
socketHandler.Proxy = new WebProxy
|
||||
{
|
||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
|
||||
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
|
||||
};
|
||||
}
|
||||
return socketHandler;
|
||||
@ -143,12 +150,12 @@ namespace CryptoExchange.Net
|
||||
catch (PlatformNotSupportedException) { }
|
||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||
|
||||
if (proxy != null)
|
||||
if (options.Proxy != null)
|
||||
{
|
||||
httpHandler.Proxy = new WebProxy
|
||||
{
|
||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
|
||||
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
|
||||
};
|
||||
}
|
||||
return httpHandler;
|
||||
|
||||
@ -154,6 +154,9 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public CallResult AsDataless()
|
||||
{
|
||||
if (Error != null )
|
||||
return new CallResult(Error);
|
||||
|
||||
return SuccessResult;
|
||||
}
|
||||
|
||||
@ -531,11 +534,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeMode">Trade mode the result applies to</param>
|
||||
/// <param name="data">Data</param>
|
||||
/// <param name="nextPageToken">Next page token</param>
|
||||
/// <param name="nextPageRequest">Next page request</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null)
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageToken);
|
||||
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -545,11 +548,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="exchange">The exchange</param>
|
||||
/// <param name="tradeModes">Trade modes the result applies to</param>
|
||||
/// <param name="data">Data</param>
|
||||
/// <param name="nextPageToken">Next page token</param>
|
||||
/// <param name="nextPageRequest">Next page token</param>
|
||||
/// <returns></returns>
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null)
|
||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
||||
{
|
||||
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageToken);
|
||||
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -130,7 +130,8 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
@ -211,7 +212,15 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||
public DeserializeError(string? message = null, Exception? exception = null)
|
||||
: base(null,
|
||||
_errorInfo with
|
||||
{
|
||||
Message = message?.Length > 0
|
||||
? message
|
||||
: _errorInfo.Message
|
||||
},
|
||||
exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for API usage
|
||||
@ -17,10 +15,5 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// Note that this comes at a performance cost
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
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>
|
||||
/// Whether or not client side rate limiting should be applied
|
||||
/// </summary>
|
||||
@ -45,7 +40,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,10 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// Library options
|
||||
/// </summary>
|
||||
/// <typeparam name="TRestOptions"></typeparam>
|
||||
/// <typeparam name="TSocketOptions"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
where TRestOptions: RestExchangeOptions, new()
|
||||
where TSocketOptions: SocketExchangeOptions, new()
|
||||
where TApiCredentials: ApiCredentials
|
||||
where TEnvironment: TradeEnvironment
|
||||
public class LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
|
||||
where TRestOptions : RestExchangeOptions<TEnvironment>, new()
|
||||
where TSocketOptions : SocketExchangeOptions<TEnvironment>, new()
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Rest client options
|
||||
@ -31,11 +26,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public TEnvironment? Environment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests.
|
||||
/// </summary>
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The DI service lifetime for the socket client
|
||||
/// </summary>
|
||||
@ -44,9 +34,8 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// Copy values from these options to the target options
|
||||
/// </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.SocketClientLifeTime = SocketClientLifeTime;
|
||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||
@ -55,4 +44,29 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,27 +18,10 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public T Set<T>(T item) where T : RestApiOptions, new()
|
||||
{
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,9 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
public class RestExchangeOptions: ExchangeOptions
|
||||
public class RestExchangeOptions : ExchangeOptions
|
||||
{
|
||||
|
||||
/// <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
|
||||
/// </summary>
|
||||
@ -32,10 +33,29 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
#else
|
||||
= new Version(1, 1);
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Http client keep alive interval for keeping connections open
|
||||
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
|
||||
/// </summary>
|
||||
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
|
||||
#if NET5_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// Enable multiple simultaneous HTTP 2 connections. Only applied when using dependency injection
|
||||
/// </summary>
|
||||
public bool HttpEnableMultipleHttp2Connections { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Lifetime of pooled HTTP connections; the time before a connection is recreated. Only applied when using dependency injection
|
||||
/// </summary>
|
||||
public TimeSpan HttpPooledConnectionLifetime { get; set; } = TimeSpan.FromMinutes(15);
|
||||
/// <summary>
|
||||
/// Idle timeout of pooled HTTP connections; the time before an open connection is closed when there are no requests. Only applied when using dependency injection
|
||||
/// </summary>
|
||||
public TimeSpan HttpPooledConnectionIdleTimeout { get; set; } = TimeSpan.FromMinutes(2);
|
||||
/// <summary>
|
||||
/// Max number of connections per server. Only applied when using dependency injection
|
||||
/// </summary>
|
||||
public int HttpMaxConnectionsPerServer { get; set; } = int.MaxValue;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Set the values of this options on the target options
|
||||
@ -45,7 +65,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
@ -54,15 +73,19 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
item.HttpVersion = HttpVersion;
|
||||
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||
#if NET5_0_OR_GREATER
|
||||
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
|
||||
item.HttpPooledConnectionLifetime = HttpPooledConnectionLifetime;
|
||||
item.HttpPooledConnectionIdleTimeout = HttpPooledConnectionIdleTimeout;
|
||||
item.HttpEnableMultipleHttp2Connections = HttpEnableMultipleHttp2Connections;
|
||||
#endif
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
|
||||
/// <inheritdoc />
|
||||
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
@ -83,20 +106,32 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
/// <inheritdoc />
|
||||
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment>
|
||||
where TEnvironment : TradeEnvironment
|
||||
where TApiCredentials : ApiCredentials
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </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;
|
||||
set => base.ApiCredentials = value;
|
||||
base.Set(item);
|
||||
item.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public T Set<T>(T item) where T : SocketApiOptions, new()
|
||||
{
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
@ -32,20 +31,4 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +91,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <returns></returns>
|
||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||
{
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.ReconnectPolicy = ReconnectPolicy;
|
||||
@ -110,12 +109,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
|
||||
/// <inheritdoc />
|
||||
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions
|
||||
where TEnvironment : TradeEnvironment
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 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`
|
||||
@ -138,17 +136,29 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment>
|
||||
where TEnvironment : TradeEnvironment
|
||||
where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </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;
|
||||
set => base.ApiCredentials = value;
|
||||
base.Set(item);
|
||||
item.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,22 +6,27 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// <summary>
|
||||
/// Options to update
|
||||
/// </summary>
|
||||
public class UpdateOptions<T> where T : ApiCredentials
|
||||
public class UpdateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
/// <summary>
|
||||
/// Api credentials
|
||||
/// </summary>
|
||||
public T? ApiCredentials { get; set; }
|
||||
/// <summary>
|
||||
/// Request timeout
|
||||
/// </summary>
|
||||
public TimeSpan? RequestTimeout { get; set; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
|
||||
/// <summary>
|
||||
/// Options to update
|
||||
/// </summary>
|
||||
public class UpdateOptions<TApiCredentials>: UpdateOptions
|
||||
where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials
|
||||
/// </summary>
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the endpoint path should always include the trailing `/`
|
||||
/// </summary>
|
||||
public bool? ForcePathEndWithSlash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
HttpMethod method,
|
||||
@ -59,8 +60,9 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
|
||||
bool? tryParseOnNonSuccess = null,
|
||||
bool? forcePathEndWithSlash = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
|
||||
|
||||
/// <summary>
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
@ -77,6 +79,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
string identifier,
|
||||
@ -90,7 +93,8 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null)
|
||||
bool? tryParseOnNonSuccess = null,
|
||||
bool? forcePathEndWithSlash = null)
|
||||
{
|
||||
|
||||
if (!_definitions.TryGetValue(identifier, out var def))
|
||||
@ -105,7 +109,8 @@ namespace CryptoExchange.Net.Objects
|
||||
RequestBodyFormat = requestBodyFormat,
|
||||
ParameterPosition = parameterPosition,
|
||||
PreventCaching = preventCaching ?? false,
|
||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
|
||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
|
||||
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
|
||||
};
|
||||
_definitions.TryAdd(identifier, def);
|
||||
}
|
||||
|
||||
@ -17,11 +17,11 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
||||
/// <param name="path"></param>
|
||||
public PathStartFilter(string path)
|
||||
{
|
||||
_path = path;
|
||||
_path = path.TrimStart('/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
{
|
||||
@ -36,6 +37,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
private readonly double? _decayRate;
|
||||
private readonly int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||
private readonly SemaphoreSlim? _sharedGuardSemaphore;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "RateLimitGuard";
|
||||
@ -52,6 +54,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// </summary>
|
||||
public TimeSpan TimeSpan { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this guard is shared between multiple gates
|
||||
/// </summary>
|
||||
public bool SharedGuard { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@ -62,8 +69,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
|
||||
{
|
||||
}
|
||||
|
||||
@ -77,22 +85,27 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="windowType">Type of rate limit window</param>
|
||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||
/// <param name="shared">Whether this guard is shared between multiple gates</param>
|
||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
_windowType = windowType;
|
||||
Limit = limit;
|
||||
TimeSpan = timeSpan;
|
||||
SharedGuard = shared;
|
||||
_keySelector = keySelector;
|
||||
_decayRate = decayPerTimeSpan;
|
||||
_connectionWeight = connectionWeight;
|
||||
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||
{
|
||||
foreach(var filter in _filters)
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
if (!filter.Passes(type, definition, host, apiKey))
|
||||
return LimitCheck.NotApplicable;
|
||||
@ -101,18 +114,30 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
if (type == RateLimitItemType.Connection)
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
_trackers.Add(key, tracker);
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
{
|
||||
tracker = CreateTracker();
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Release();
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -127,9 +152,23 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
if (type == RateLimitItemType.Connection)
|
||||
requestWeight = _connectionWeight ?? requestWeight;
|
||||
|
||||
|
||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||
var tracker = _trackers[key];
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Wait();
|
||||
|
||||
try
|
||||
{
|
||||
tracker.ApplyWeight(requestWeight);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (SharedGuard)
|
||||
_sharedGuardSemaphore!.Release();
|
||||
}
|
||||
|
||||
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,6 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <summary>
|
||||
/// Create request object for web request
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="requestId"></param>
|
||||
public Request(HttpRequestMessage request, HttpClient client, int requestId)
|
||||
{
|
||||
_httpClient = client;
|
||||
@ -55,10 +52,12 @@ namespace CryptoExchange.Net.Requests
|
||||
public int RequestId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContent(string data, string contentType)
|
||||
public void SetContent(string data, Encoding? encoding, string contentType)
|
||||
{
|
||||
Content = data;
|
||||
_request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
_request.Content = new StringContent(data, encoding ?? Encoding.UTF8, contentType);
|
||||
if (encoding == null)
|
||||
_request.Content.Headers.ContentType!.CharSet = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
@ -12,14 +13,16 @@ namespace CryptoExchange.Net.Requests
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
private HttpClient? _httpClient;
|
||||
private RestExchangeOptions? _options;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
||||
{
|
||||
if (client == null)
|
||||
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
|
||||
client = CreateClient(options);
|
||||
|
||||
_httpClient = client;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -39,15 +42,20 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <inheritdoc />
|
||||
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||
{
|
||||
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
|
||||
var newOptions = new RestExchangeOptions();
|
||||
_options!.Set(newOptions);
|
||||
newOptions.Proxy = proxy;
|
||||
newOptions.RequestTimeout = requestTimeout;
|
||||
newOptions.HttpKeepAliveInterval = httpKeepAliveInterval;
|
||||
_httpClient = CreateClient(newOptions);
|
||||
}
|
||||
|
||||
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||
private static HttpClient CreateClient(RestExchangeOptions options)
|
||||
{
|
||||
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
|
||||
var handler = LibraryHelpers.CreateHttpClientMessageHandler(options);
|
||||
var client = new HttpClient(handler)
|
||||
{
|
||||
Timeout = requestTimeout
|
||||
Timeout = options.RequestTimeout
|
||||
};
|
||||
return client;
|
||||
}
|
||||
|
||||
@ -16,6 +16,11 @@
|
||||
/// <summary>
|
||||
/// Order has been canceled
|
||||
/// </summary>
|
||||
Canceled
|
||||
Canceled,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown/unmapped status
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,11 @@
|
||||
/// <summary>
|
||||
/// Completed
|
||||
/// </summary>
|
||||
Completed
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown/unmapped status
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,11 @@
|
||||
/// <summary>
|
||||
/// Trigger order has been triggered. Resulting order might be filled or not.
|
||||
/// </summary>
|
||||
Triggered
|
||||
Triggered,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown/unmapped status
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// A token which a request can use to retrieve the next page if there are more pages in the result set
|
||||
/// </summary>
|
||||
public interface INextPageToken
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A datetime offset token
|
||||
/// </summary>
|
||||
public record DateTimeToken: INextPageToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Last result time
|
||||
/// </summary>
|
||||
public DateTime LastTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public DateTimeToken(DateTime timestamp)
|
||||
{
|
||||
LastTime = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A current page index token
|
||||
/// </summary>
|
||||
public record PageToken: INextPageToken
|
||||
{
|
||||
/// <summary>
|
||||
/// The next page index
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
/// <summary>
|
||||
/// Page size
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PageToken(int page, int pageSize)
|
||||
{
|
||||
Page = page;
|
||||
PageSize = pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A id offset token
|
||||
/// </summary>
|
||||
public record FromIdToken : INextPageToken
|
||||
{
|
||||
/// <summary>
|
||||
/// The last id from previous result
|
||||
/// </summary>
|
||||
public string FromToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public FromIdToken(string fromToken)
|
||||
{
|
||||
FromToken = fromToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A cursor token
|
||||
/// </summary>
|
||||
public record CursorToken : INextPageToken
|
||||
{
|
||||
/// <summary>
|
||||
/// The next page cursor
|
||||
/// </summary>
|
||||
public string Cursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CursorToken(string cursor)
|
||||
{
|
||||
Cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A result offset token
|
||||
/// </summary>
|
||||
public record OffsetToken : INextPageToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Offset in the result set
|
||||
/// </summary>
|
||||
public int Offset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public OffsetToken(int offset)
|
||||
{
|
||||
Offset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get funding rate records
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,14 +73,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Spot get closed orders request options
|
||||
/// </summary>
|
||||
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedFuturesOrdersOptions { get; }
|
||||
GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
|
||||
/// <summary>
|
||||
/// Get info on closed futures orders
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Futures get order trades request options
|
||||
@ -96,14 +96,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Futures user trades request options
|
||||
/// </summary>
|
||||
PaginatedEndpointOptions<GetUserTradesRequest> GetFuturesUserTradesOptions { get; }
|
||||
GetUserTradesOptions GetFuturesUserTradesOptions { get; }
|
||||
/// <summary>
|
||||
/// Get futures user trade records
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Futures cancel order request options
|
||||
|
||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get index price kline/candlestick data
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get mark price kline/candlestick data
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get position history
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,9 +30,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get deposit records
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get kline/candlestick data
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get public trade history
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get withdrawal records
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,14 +72,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Spot get closed orders request options
|
||||
/// </summary>
|
||||
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedSpotOrdersOptions { get; }
|
||||
GetClosedOrdersOptions GetClosedSpotOrdersOptions { get; }
|
||||
/// <summary>
|
||||
/// Get info on closed spot orders
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Spot get order trades request options
|
||||
@ -95,14 +95,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Spot user trades request options
|
||||
/// </summary>
|
||||
PaginatedEndpointOptions<GetUserTradesRequest> GetSpotUserTradesOptions { get; }
|
||||
GetUserTradesOptions GetSpotUserTradesOptions { get; }
|
||||
/// <summary>
|
||||
/// Get spot user trade records
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Spot cancel order request options
|
||||
|
||||
@ -24,9 +24,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public TradingMode[]? DataTradeMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Token to retrieve the next page with
|
||||
/// Next page request, can be passed to the next request on the same endpoint to get the next page
|
||||
/// </summary>
|
||||
public INextPageToken? NextPageToken { get; }
|
||||
public PageRequest? NextPageRequest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -46,7 +46,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
string exchange,
|
||||
TradingMode dataTradeMode,
|
||||
WebCallResult<T> result,
|
||||
INextPageToken? nextPageToken = null) :
|
||||
PageRequest? nextPageToken = null) :
|
||||
base(result.ResponseStatusCode,
|
||||
result.HttpVersion,
|
||||
result.ResponseHeaders,
|
||||
@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
DataTradeMode = new[] { dataTradeMode };
|
||||
Exchange = exchange;
|
||||
NextPageToken = nextPageToken;
|
||||
NextPageRequest = nextPageToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -74,7 +74,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
string exchange,
|
||||
TradingMode[]? dataTradeModes,
|
||||
WebCallResult<T> result,
|
||||
INextPageToken? nextPageToken = null) :
|
||||
PageRequest? nextPageRequest = null) :
|
||||
base(result.ResponseStatusCode,
|
||||
result.HttpVersion,
|
||||
result.ResponseHeaders,
|
||||
@ -92,7 +92,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
DataTradeMode = dataTradeModes;
|
||||
Exchange = exchange;
|
||||
NextPageToken = nextPageToken;
|
||||
NextPageRequest = nextPageRequest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -115,7 +115,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
ResultDataSource dataSource,
|
||||
[AllowNull] T data,
|
||||
Error? error,
|
||||
INextPageToken? nextPageToken = null) : base(
|
||||
PageRequest? nextPageToken = null) : base(
|
||||
code,
|
||||
httpVersion,
|
||||
responseHeaders,
|
||||
@ -133,7 +133,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
DataTradeMode = dataTradeModes;
|
||||
Exchange = exchange;
|
||||
NextPageToken = nextPageToken;
|
||||
NextPageRequest = nextPageToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -144,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <returns></returns>
|
||||
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
||||
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageRequest);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public class GetClosedOrdersOptions : PaginatedEndpointOptions<GetClosedOrdersRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the start/end time filter is supported
|
||||
/// </summary>
|
||||
public bool TimeFilterSupported { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
public GetClosedOrdersOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!TimeFilterSupported && request.StartTime != null)
|
||||
return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported");
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public class GetDepositsOptions : PaginatedEndpointOptions<GetDepositsRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the start/end time filter is supported
|
||||
/// </summary>
|
||||
public bool TimeFilterSupported { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
public GetDepositsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!TimeFilterSupported && request.StartTime != null)
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for requesting funding rate history
|
||||
@ -8,8 +12,43 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
public GetFundingRateHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetFundingRateHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,15 +18,12 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of data points which can be requested
|
||||
/// </summary>
|
||||
public int? MaxTotalDataPoints { get; set; }
|
||||
/// <summary>
|
||||
/// The max age of the data that can be requested
|
||||
/// </summary>
|
||||
public TimeSpan? MaxAge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
SupportIntervals = new[]
|
||||
{
|
||||
@ -50,7 +47,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
SupportIntervals = intervals;
|
||||
}
|
||||
@ -68,12 +66,29 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (!IsSupported(request.Interval))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported");
|
||||
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
|
||||
|
||||
if (request.Limit > MaxLimit)
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction == DataDirection.Ascending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
if (MaxTotalDataPoints.HasValue)
|
||||
{
|
||||
if (request.Limit > MaxTotalDataPoints.Value)
|
||||
@ -93,6 +108,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}");
|
||||
if (MaxAge != null)
|
||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for requesting position history
|
||||
@ -8,8 +12,43 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
public GetPositionHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetPositionHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,34 +9,27 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public class GetTradeHistoryOptions : PaginatedEndpointOptions<GetTradeHistoryRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// The max age of data that can be requested
|
||||
/// </summary>
|
||||
public TimeSpan? MaxAge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
public GetTradeHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available");
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
if (MaxAge != null)
|
||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for requesting user trades
|
||||
/// </summary>
|
||||
public class GetUserTradesOptions : PaginatedEndpointOptions<GetUserTradesRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetUserTradesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetUserTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public class GetWithdrawalsOptions : PaginatedEndpointOptions<GetWithdrawalsRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the start/end time filter is supported
|
||||
/// </summary>
|
||||
public bool TimeFilterSupported { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
public GetWithdrawalsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!TimeFilterSupported && request.StartTime != null)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported");
|
||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||
|
||||
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||
|
||||
if (!TimePeriodFilterSupport)
|
||||
{
|
||||
// When going descending we can still allow startTime filter to limit the results
|
||||
var now = DateTime.UtcNow;
|
||||
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@ -14,9 +15,13 @@ namespace CryptoExchange.Net.SharedApis
|
||||
#endif
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of pagination supported
|
||||
/// Whether ascending data retrieval and pagination is available
|
||||
/// </summary>
|
||||
public SharedPaginationSupport PaginationSupport { get; }
|
||||
public bool SupportsAscending { get; set; }
|
||||
/// <summary>
|
||||
/// Whether ascending data retrieval and pagination is available
|
||||
/// </summary>
|
||||
public bool SupportsDescending { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether filtering based on start/end time is supported
|
||||
@ -28,12 +33,23 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public int MaxLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max age of data that can be requested
|
||||
/// </summary>
|
||||
public TimeSpan? MaxAge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
|
||||
public PaginatedEndpointOptions(
|
||||
bool supportsAscending,
|
||||
bool supportsDescending,
|
||||
bool timePeriodSupport,
|
||||
int maxLimit,
|
||||
bool needsAuthentication) : base(needsAuthentication)
|
||||
{
|
||||
PaginationSupport = paginationType;
|
||||
SupportsAscending = supportsAscending;
|
||||
SupportsDescending = supportsDescending;
|
||||
TimePeriodFilterSupport = timePeriodSupport;
|
||||
MaxLimit = maxLimit;
|
||||
}
|
||||
@ -42,9 +58,11 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override string ToString(string exchange)
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Pagination type: {PaginationSupport}");
|
||||
sb.AppendLine($"Ascending retrieval supported: {SupportsAscending}");
|
||||
sb.AppendLine($"Descending retrieval supported: {SupportsDescending}");
|
||||
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
||||
sb.AppendLine($"Max limit: {MaxLimit}");
|
||||
sb.AppendLine($"Max age: {MaxAge}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public enum DataDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Old to new order
|
||||
/// </summary>
|
||||
Ascending,
|
||||
/// <summary>
|
||||
/// New to old order
|
||||
/// </summary>
|
||||
Descending
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Next page request info
|
||||
/// </summary>
|
||||
public class PageRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Pagination cursor
|
||||
/// </summary>
|
||||
public string? Cursor { get; set; }
|
||||
/// <summary>
|
||||
/// Page number
|
||||
/// </summary>
|
||||
public int? Page { get; set; }
|
||||
/// <summary>
|
||||
/// Result offset
|
||||
/// </summary>
|
||||
public int? Offset { get; set; }
|
||||
/// <summary>
|
||||
/// From id filter
|
||||
/// </summary>
|
||||
public string? FromId { get; set; }
|
||||
/// <summary>
|
||||
/// Start time filter
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
/// <summary>
|
||||
/// End time filter
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
}
|
||||
405
CryptoExchange.Net/SharedApis/Models/Pagination/Pagination.cs
Normal file
405
CryptoExchange.Net/SharedApis/Models/Pagination/Pagination.cs
Normal file
@ -0,0 +1,405 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Pagination methods
|
||||
/// </summary>
|
||||
public static class Pagination
|
||||
{
|
||||
/// <summary>
|
||||
/// Get pagination parameters
|
||||
/// </summary>
|
||||
/// <param name="direction">The data direction</param>
|
||||
/// <param name="limit">Result limit</param>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="paginationRequest">Provided page request</param>
|
||||
/// <param name="setOtherTimeLimiter">Whether to set start time if direction is descending, or end time if direction is ascending</param>
|
||||
/// <param name="maxPeriod">Max period the time filters can span</param>
|
||||
/// <returns></returns>
|
||||
public static PaginationParameters GetPaginationParameters(
|
||||
DataDirection direction,
|
||||
int limit,
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
PageRequest? paginationRequest,
|
||||
bool setOtherTimeLimiter = true,
|
||||
TimeSpan? maxPeriod = null
|
||||
)
|
||||
{
|
||||
var startTime = paginationRequest?.StartTime ?? requestStartTime;
|
||||
var endTime = paginationRequest?.EndTime ?? requestEndTime;
|
||||
if (maxPeriod != null)
|
||||
{
|
||||
if (direction == DataDirection.Ascending)
|
||||
{
|
||||
if (startTime == null)
|
||||
{
|
||||
startTime = endTime.Add(-maxPeriod.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
endTime = startTime.Value.Add(maxPeriod.Value);
|
||||
if (endTime > DateTime.UtcNow)
|
||||
endTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
startTime = endTime.Add(-maxPeriod.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return new PaginationParameters
|
||||
{
|
||||
Limit = limit,
|
||||
StartTime = direction == DataDirection.Ascending || setOtherTimeLimiter ? startTime : null,
|
||||
EndTime = direction == DataDirection.Descending || setOtherTimeLimiter ? endTime : null,
|
||||
Direction = direction,
|
||||
FromId = paginationRequest?.FromId,
|
||||
Offset = paginationRequest?.Offset,
|
||||
Page = paginationRequest?.Page,
|
||||
Cursor = paginationRequest?.Cursor
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next page request parameters from result kline data
|
||||
/// </summary>
|
||||
/// <param name="nextPageRequest">Callback for returning the next page request</param>
|
||||
/// <param name="resultCount">Number of results in data</param>
|
||||
/// <param name="timestamps">Timestamps of the result data</param>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="lastPaginationData">The last used pagination data</param>
|
||||
/// <param name="interval">Kline interval</param>
|
||||
/// <returns></returns>
|
||||
public static PageRequest? GetNextPageRequestKlines(
|
||||
Func<PageRequest?> nextPageRequest,
|
||||
int resultCount,
|
||||
IEnumerable<DateTime> timestamps,
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
PaginationParameters lastPaginationData,
|
||||
SharedKlineInterval interval
|
||||
)
|
||||
{
|
||||
if (HasNextPageKlines(resultCount, timestamps, requestStartTime, requestEndTime, lastPaginationData.Limit, lastPaginationData.Direction, interval))
|
||||
{
|
||||
var result = nextPageRequest();
|
||||
if (result != null)
|
||||
{
|
||||
result.StartTime ??= lastPaginationData.StartTime;
|
||||
result.EndTime ??= lastPaginationData.EndTime;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next page request parameters from result data
|
||||
/// </summary>
|
||||
/// <param name="nextPageRequest">Callback for returning the next page request</param>
|
||||
/// <param name="resultCount">Number of results in data</param>
|
||||
/// <param name="timestamps">Timestamps of the result data</param>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="lastPaginationData">The last used pagination data</param>
|
||||
/// <param name="maxPeriod">Max period the time filters can span</param>
|
||||
/// <param name="maxAge">Max age of the data</param>
|
||||
/// <returns></returns>
|
||||
public static PageRequest? GetNextPageRequest(
|
||||
Func<PageRequest?> nextPageRequest,
|
||||
int resultCount,
|
||||
IEnumerable<DateTime> timestamps,
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
PaginationParameters lastPaginationData,
|
||||
TimeSpan? maxPeriod = null,
|
||||
TimeSpan? maxAge = null
|
||||
)
|
||||
{
|
||||
if (HasNextPage(resultCount, timestamps, requestStartTime, requestEndTime, lastPaginationData.Limit, lastPaginationData.Direction))
|
||||
{
|
||||
var result = nextPageRequest();
|
||||
if (result != null)
|
||||
{
|
||||
result.StartTime ??= lastPaginationData.StartTime;
|
||||
result.EndTime ??= lastPaginationData.EndTime;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxPeriod != null)
|
||||
{
|
||||
if (HasNextPeriod(requestStartTime, requestEndTime, lastPaginationData.Direction, lastPaginationData, maxPeriod.Value, maxAge))
|
||||
{
|
||||
var (startTime, endTime) = GetNextPeriod(requestStartTime, requestEndTime, lastPaginationData.Direction, lastPaginationData, maxPeriod.Value, maxAge);
|
||||
return new PageRequest
|
||||
{
|
||||
StartTime = startTime,
|
||||
EndTime = endTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether there is (potentially) another page available
|
||||
/// </summary>
|
||||
/// <param name="resultCount">Number of result entries</param>
|
||||
/// <param name="timestamps">Timestamps</param>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="limit">Max number of results requested</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="interval">Kline interval</param>
|
||||
/// <returns></returns>
|
||||
public static bool HasNextPageKlines(
|
||||
int resultCount,
|
||||
IEnumerable<DateTime> timestamps,
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
int limit,
|
||||
DataDirection direction,
|
||||
SharedKlineInterval interval
|
||||
)
|
||||
{
|
||||
if (resultCount < limit)
|
||||
return false;
|
||||
|
||||
if (direction == DataDirection.Ascending)
|
||||
{
|
||||
if (timestamps.Max().AddSeconds((int)interval) >= requestEndTime)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (timestamps.Min().AddSeconds((int)interval) < requestStartTime)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether there is (potentially) another page available
|
||||
/// </summary>
|
||||
/// <param name="resultCount">Number of result entries</param>
|
||||
/// <param name="timestamps">Timestamps</param>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="limit">Max number of results requested</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <returns></returns>
|
||||
public static bool HasNextPage(
|
||||
int resultCount,
|
||||
IEnumerable<DateTime> timestamps,
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
int limit,
|
||||
DataDirection direction)
|
||||
{
|
||||
if (resultCount < limit)
|
||||
return false;
|
||||
|
||||
if (!timestamps.Any())
|
||||
return false;
|
||||
|
||||
if (direction == DataDirection.Ascending)
|
||||
{
|
||||
if (timestamps.Max() >= requestEndTime)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (timestamps.Min() < requestStartTime)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next page PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromPage(PaginationParameters lastPaginationData)
|
||||
{
|
||||
return new PageRequest { Page = (lastPaginationData.Page ?? 1) + 1 };
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the next offset PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromOffset(PaginationParameters lastPaginationData, int resultCount)
|
||||
{
|
||||
return new PageRequest { Offset = (lastPaginationData.Offset ?? 0) + resultCount };
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the next page cursor PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromCursor(string nextCursor)
|
||||
{
|
||||
return new PageRequest { Cursor = nextCursor };
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the next id PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromId(long nextFromId)
|
||||
{
|
||||
return new PageRequest { FromId = nextFromId.ToString() };
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the next id PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromId(string nextFromId)
|
||||
{
|
||||
return new PageRequest { FromId = nextFromId };
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the next start/end time PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromTime(PaginationParameters lastPaginationData, DateTime lastTimestamp, bool setOtherTimeLimiter = true)
|
||||
{
|
||||
if (lastPaginationData.Direction == DataDirection.Ascending)
|
||||
return new PageRequest { StartTime = lastTimestamp.AddMilliseconds(1), EndTime = setOtherTimeLimiter ? lastPaginationData.EndTime : null };
|
||||
else
|
||||
return new PageRequest { EndTime = lastTimestamp.AddMilliseconds(-1), StartTime = setOtherTimeLimiter ? lastPaginationData.StartTime : null };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next start/end time klines PageRequest
|
||||
/// </summary>
|
||||
public static PageRequest NextPageFromTimeKlines(DataDirection direction, GetKlinesRequest request, DateTime lastTimestamp, int limit)
|
||||
{
|
||||
if (direction == DataDirection.Ascending)
|
||||
{
|
||||
var nextStartTime = lastTimestamp.AddSeconds((int)request.Interval);
|
||||
var endTime = nextStartTime.AddSeconds(limit * (int)request.Interval);
|
||||
var requestEndTime = request.EndTime ?? DateTime.UtcNow;
|
||||
if (endTime > requestEndTime)
|
||||
endTime = requestEndTime;
|
||||
|
||||
return new PageRequest { StartTime = nextStartTime, EndTime = endTime };
|
||||
}
|
||||
else
|
||||
{
|
||||
var nextEndTime = lastTimestamp.AddSeconds(-(int)request.Interval);
|
||||
var startTime = nextEndTime.AddSeconds(-(limit * (int)request.Interval));
|
||||
var requestStartTime = request.StartTime ?? DateTime.UtcNow;
|
||||
if (startTime < requestStartTime)
|
||||
startTime = requestStartTime;
|
||||
return new PageRequest { StartTime = startTime, EndTime = nextEndTime };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether another time period is to be requested
|
||||
/// </summary>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="lastPaginationParameters">Pagination parameters used</param>
|
||||
/// <param name="period">Max time period a request can span</param>
|
||||
/// <param name="maxAge">Max age of data that can be requested</param>
|
||||
public static bool HasNextPeriod(
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
DataDirection direction,
|
||||
PaginationParameters lastPaginationParameters,
|
||||
TimeSpan period,
|
||||
TimeSpan? maxAge)
|
||||
{
|
||||
if (direction == DataDirection.Ascending && lastPaginationParameters.StartTime == null)
|
||||
throw new InvalidOperationException("Invalid pagination data; no start time for ascending pagination");
|
||||
|
||||
if (direction == DataDirection.Ascending)
|
||||
{
|
||||
return (requestEndTime - lastPaginationParameters.EndTime!.Value).TotalSeconds > 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastPageStartTime = lastPaginationParameters.StartTime ?? lastPaginationParameters.EndTime!.Value.Add(-period);
|
||||
if (requestStartTime != null)
|
||||
{
|
||||
var nextPeriodDuration = lastPageStartTime - requestStartTime.Value;
|
||||
return nextPeriodDuration.TotalSeconds > 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var nextStartTime = lastPageStartTime - period;
|
||||
if (maxAge != null)
|
||||
{
|
||||
var minStartTime = DateTime.UtcNow - maxAge.Value;
|
||||
if ((nextStartTime.Add(period) - minStartTime).TotalSeconds < 1)
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextPeriodDuration = lastPageStartTime - nextStartTime;
|
||||
return (nextPeriodDuration).TotalSeconds > 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the start/end time for the next data period
|
||||
/// </summary>
|
||||
/// <param name="requestStartTime">User request start time</param>
|
||||
/// <param name="requestEndTime">User request end time</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="lastPaginationParameters">Pagination parameters used</param>
|
||||
/// <param name="period">Max time period a request can span</param>
|
||||
/// <param name="maxAge">Max age of data that can be requested</param>
|
||||
public static (DateTime? startTime, DateTime? endTime) GetNextPeriod(
|
||||
DateTime? requestStartTime,
|
||||
DateTime requestEndTime,
|
||||
DataDirection direction,
|
||||
PaginationParameters lastPaginationParameters,
|
||||
TimeSpan period,
|
||||
TimeSpan? maxAge
|
||||
)
|
||||
{
|
||||
DateTime? nextStartTime = null;
|
||||
DateTime? nextEndTime = null;
|
||||
if (direction == DataDirection.Ascending)
|
||||
{
|
||||
if (lastPaginationParameters.StartTime != null)
|
||||
nextStartTime = lastPaginationParameters.StartTime.Value.Add(period);
|
||||
if (lastPaginationParameters.EndTime != null)
|
||||
nextEndTime = lastPaginationParameters.EndTime.Value.Add(period);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastPaginationParameters.StartTime != null)
|
||||
nextStartTime = lastPaginationParameters.StartTime.Value.Add(-period);
|
||||
if (lastPaginationParameters.EndTime != null)
|
||||
nextEndTime = lastPaginationParameters.EndTime.Value.Add(-period);
|
||||
}
|
||||
|
||||
if (nextStartTime != null && nextStartTime < requestStartTime)
|
||||
nextStartTime = requestStartTime;
|
||||
|
||||
if (nextStartTime != null && maxAge != null && nextStartTime < DateTime.UtcNow - maxAge)
|
||||
{
|
||||
nextStartTime = DateTime.UtcNow.Add(-maxAge.Value);
|
||||
// Add 30 seconds to max sure the client/server time offset and latency doesn't push the timestamp over the limit
|
||||
nextStartTime = nextStartTime.Value.Add(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
if (nextEndTime != null && nextEndTime > requestEndTime)
|
||||
nextEndTime = requestEndTime;
|
||||
|
||||
return (nextStartTime, nextEndTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Pagination parameters
|
||||
/// </summary>
|
||||
public record PaginationParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection Direction { get; set; }
|
||||
/// <summary>
|
||||
/// Start time filter
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
/// <summary>
|
||||
/// End time filter
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
/// <summary>
|
||||
/// Id filter
|
||||
/// </summary>
|
||||
public string? FromId { get; set; }
|
||||
/// <summary>
|
||||
/// Result offset
|
||||
/// </summary>
|
||||
public int? Offset { get; set; }
|
||||
/// <summary>
|
||||
/// Page number
|
||||
/// </summary>
|
||||
public int? Page { get; set; }
|
||||
/// <summary>
|
||||
/// Pagination cursor
|
||||
/// </summary>
|
||||
public string? Cursor { get; set; }
|
||||
/// <summary>
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int Limit { get; set; }
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
Asset = asset;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -32,13 +36,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
Interval = interval;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -35,13 +39,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
Symbol = symbol;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -51,13 +57,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
TradingMode = tradeMode;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,15 +10,19 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Filter by start time
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; }
|
||||
public DateTime StartTime { get; set; }
|
||||
/// <summary>
|
||||
/// Filter by end time
|
||||
/// </summary>
|
||||
public DateTime EndTime { get; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
/// <summary>
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; }
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime endTime, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Max number of results
|
||||
/// </summary>
|
||||
public int? Limit { get; }
|
||||
/// <summary>
|
||||
/// Data direction
|
||||
/// </summary>
|
||||
public DataDirection? Direction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="startTime">Filter by start time</param>
|
||||
/// <param name="endTime">Filter by end time</param>
|
||||
/// <param name="limit">Max number of results</param>
|
||||
/// <param name="direction">Data direction</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
Asset = asset;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
Limit = limit;
|
||||
Direction = direction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,9 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||
using CryptoExchange.Net.Sockets.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
@ -123,8 +125,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
|
||||
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,8 +136,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
|
||||
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,8 +240,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToArray()!;
|
||||
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToArray()!;
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,8 +251,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
||||
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,7 +262,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
#else
|
||||
private readonly object _listenersLock = new object();
|
||||
#endif
|
||||
private readonly List<IMessageProcessor> _listeners;
|
||||
private ReadOnlyCollection<IMessageProcessor> _listeners;
|
||||
private readonly ILogger _logger;
|
||||
private SocketStatus _status;
|
||||
|
||||
@ -313,7 +311,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
_socket.OnError += HandleErrorAsync;
|
||||
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
||||
|
||||
_listeners = new List<IMessageProcessor>();
|
||||
_listeners = new ReadOnlyCollection<IMessageProcessor>([]);
|
||||
|
||||
_serializer = apiClient.CreateSerializer();
|
||||
}
|
||||
@ -340,21 +338,18 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||
|
||||
lock (_listenersLock)
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
|
||||
{
|
||||
subscription.IsClosingConnection = true;
|
||||
subscription.Reset();
|
||||
}
|
||||
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
{
|
||||
query.Fail(new WebError("Connection interrupted"));
|
||||
_listeners.Remove(query);
|
||||
}
|
||||
subscription.IsClosingConnection = true;
|
||||
subscription.Reset();
|
||||
}
|
||||
|
||||
var queryList = _listeners.OfType<Query>().ToList();
|
||||
foreach (var query in queryList)
|
||||
query.Fail(new WebError("Connection interrupted"));
|
||||
|
||||
RemoveMessageProcessors(queryList);
|
||||
|
||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@ -369,17 +364,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
Authenticated = false;
|
||||
_lastSequenceNumber = 0;
|
||||
|
||||
lock (_listenersLock)
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||
subscription.Reset();
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||
subscription.Reset();
|
||||
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
{
|
||||
query.Fail(new WebError("Connection interrupted"));
|
||||
_listeners.Remove(query);
|
||||
}
|
||||
}
|
||||
var queryList = _listeners.OfType<Query>().ToList();
|
||||
foreach (var query in queryList)
|
||||
query.Fail(new WebError("Connection interrupted"));
|
||||
|
||||
RemoveMessageProcessors(queryList);
|
||||
|
||||
_ = Task.Run(() => ConnectionLost?.Invoke());
|
||||
return Task.CompletedTask;
|
||||
@ -401,14 +393,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
Status = SocketStatus.Resubscribing;
|
||||
|
||||
lock (_listenersLock)
|
||||
{
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
{
|
||||
query.Fail(new WebError("Connection interrupted"));
|
||||
_listeners.Remove(query);
|
||||
}
|
||||
}
|
||||
var queryList = _listeners.OfType<Query>().ToList();
|
||||
foreach (var query in queryList)
|
||||
query.Fail(new WebError("Connection interrupted"));
|
||||
|
||||
RemoveMessageProcessors(queryList);
|
||||
|
||||
// Can't wait for this as it would cause a deadlock
|
||||
_ = Task.Run(async () =>
|
||||
@ -463,12 +452,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <returns></returns>
|
||||
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
|
||||
{
|
||||
Query? query;
|
||||
lock (_listenersLock)
|
||||
{
|
||||
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||
}
|
||||
|
||||
var query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||
if (query == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
@ -492,12 +476,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <param name="requestId">Id of the request sent</param>
|
||||
protected virtual Task HandleRequestSentAsync(int requestId)
|
||||
{
|
||||
Query? query;
|
||||
lock (_listenersLock)
|
||||
{
|
||||
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||
}
|
||||
|
||||
var query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||
if (query == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
@ -543,22 +522,19 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
}
|
||||
|
||||
Type? deserializationType = null;
|
||||
lock (_listenersLock)
|
||||
foreach (var subscription in _listeners)
|
||||
{
|
||||
foreach (var subscription in _listeners)
|
||||
foreach (var route in subscription.MessageRouter.Routes)
|
||||
{
|
||||
foreach (var route in subscription.MessageRouter.Routes)
|
||||
{
|
||||
if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
deserializationType = route.DeserializationType;
|
||||
break;
|
||||
}
|
||||
|
||||
if (deserializationType != null)
|
||||
break;
|
||||
deserializationType = route.DeserializationType;
|
||||
break;
|
||||
}
|
||||
|
||||
if (deserializationType != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (deserializationType == null)
|
||||
@ -605,84 +581,69 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
var topicFilter = messageConverter.GetTopicFilter(result);
|
||||
|
||||
bool processed = false;
|
||||
lock (_listenersLock)
|
||||
foreach (var processor in _listeners)
|
||||
{
|
||||
var currentCount = _listeners.Count;
|
||||
for(var i = 0; i < _listeners.Count; i++)
|
||||
bool isQuery = false;
|
||||
Query? query = null;
|
||||
if (processor is Query cquery)
|
||||
{
|
||||
if (_listeners.Count != currentCount)
|
||||
{
|
||||
// Possible a query added or removed. If added it's not a problem, if removed it is
|
||||
if (_listeners.Count < currentCount)
|
||||
throw new Exception("Listeners list adjusted, can't continue processing");
|
||||
}
|
||||
|
||||
var processor = _listeners[i];
|
||||
bool isQuery = false;
|
||||
Query? query = null;
|
||||
if (processor is Query cquery)
|
||||
{
|
||||
isQuery = true;
|
||||
query = cquery;
|
||||
}
|
||||
|
||||
var complete = false;
|
||||
|
||||
foreach (var route in processor.MessageRouter.Routes)
|
||||
{
|
||||
if (route.TypeIdentifier != typeIdentifier)
|
||||
continue;
|
||||
|
||||
// Forward message rules:
|
||||
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
||||
// | N | N | - | Y | No topic filter applied
|
||||
// | N | Y | - | N | Route only listens to specific topic
|
||||
// | Y | N | - | Y | Route listens to all message regardless of topic
|
||||
// | Y | Y | Y | Y | Route listens to specific message topic
|
||||
// | Y | Y | N | N | Route listens to different topic
|
||||
if (topicFilter == null)
|
||||
{
|
||||
if (route.TopicFilter != null)
|
||||
// No topic on message, but route is filtering on topic
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
||||
// Message has a topic, and the route has a filter for another topic
|
||||
continue;
|
||||
}
|
||||
|
||||
processed = true;
|
||||
|
||||
if (isQuery && query!.Completed)
|
||||
continue;
|
||||
|
||||
processor.Handle(this, receiveTime, originalData, result, route);
|
||||
if (isQuery && !route.MultipleReaders)
|
||||
{
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (complete)
|
||||
break;
|
||||
isQuery = true;
|
||||
query = cquery;
|
||||
}
|
||||
|
||||
var complete = false;
|
||||
|
||||
foreach (var route in processor.MessageRouter.Routes)
|
||||
{
|
||||
if (route.TypeIdentifier != typeIdentifier)
|
||||
continue;
|
||||
|
||||
// Forward message rules:
|
||||
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
||||
// | N | N | - | Y | No topic filter applied
|
||||
// | N | Y | - | N | Route only listens to specific topic
|
||||
// | Y | N | - | Y | Route listens to all message regardless of topic
|
||||
// | Y | Y | Y | Y | Route listens to specific message topic
|
||||
// | Y | Y | N | N | Route listens to different topic
|
||||
if (topicFilter == null)
|
||||
{
|
||||
if (route.TopicFilter != null)
|
||||
// No topic on message, but route is filtering on topic
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
||||
// Message has a topic, and the route has a filter for another topic
|
||||
continue;
|
||||
}
|
||||
|
||||
processed = true;
|
||||
|
||||
if (isQuery && query!.Completed)
|
||||
continue;
|
||||
|
||||
processor.Handle(this, receiveTime, originalData, result, route);
|
||||
if (isQuery && !route.MultipleReaders)
|
||||
{
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (complete)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!processed)
|
||||
{
|
||||
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||
{
|
||||
lock (_listenersLock)
|
||||
{
|
||||
_logger.ReceivedMessageNotMatchedToAnyListener(
|
||||
SocketId,
|
||||
typeIdentifier,
|
||||
topicFilter!,
|
||||
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
||||
}
|
||||
_logger.ReceivedMessageNotMatchedToAnyListener(
|
||||
SocketId,
|
||||
typeIdentifier,
|
||||
topicFilter!,
|
||||
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -727,13 +688,10 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||
|
||||
lock (_listenersLock)
|
||||
foreach (var subscription in _listeners.OfType<Subscription>())
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>())
|
||||
{
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
}
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
}
|
||||
|
||||
await _socket.CloseAsync().ConfigureAwait(false);
|
||||
@ -763,20 +721,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
|
||||
bool anyDuplicateSubscription;
|
||||
lock (_listenersLock)
|
||||
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (_listenersLock)
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
bool anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
||||
bool shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
{
|
||||
bool needUnsub;
|
||||
lock (_listenersLock)
|
||||
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
||||
|
||||
var needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
||||
if (needUnsub && _socket.IsOpen)
|
||||
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||
}
|
||||
@ -800,8 +750,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
lock (_listenersLock)
|
||||
_listeners.Remove(subscription);
|
||||
RemoveMessageProcessor(subscription);
|
||||
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
}
|
||||
@ -825,8 +774,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
||||
return false;
|
||||
|
||||
lock (_listenersLock)
|
||||
_listeners.Add(subscription);
|
||||
AddMessageProcessor(subscription);
|
||||
|
||||
if (subscription.UserSubscription)
|
||||
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
||||
@ -839,8 +787,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <param name="id"></param>
|
||||
public Subscription? GetSubscription(int id)
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
|
||||
return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -888,15 +835,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
|
||||
private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default)
|
||||
{
|
||||
lock (_listenersLock)
|
||||
_listeners.Add(query);
|
||||
|
||||
AddMessageProcessor(query);
|
||||
var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false);
|
||||
if (!sendResult)
|
||||
{
|
||||
query.Fail(sendResult.Error!);
|
||||
lock (_listenersLock)
|
||||
_listeners.Remove(query);
|
||||
RemoveMessageProcessor(query);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -927,8 +871,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_listenersLock)
|
||||
_listeners.Remove(query);
|
||||
RemoveMessageProcessor(query);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1034,9 +977,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
|
||||
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||
{
|
||||
bool anySubscriptions;
|
||||
lock (_listenersLock)
|
||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||
var anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||
if (!anySubscriptions)
|
||||
{
|
||||
// No need to resubscribe anything
|
||||
@ -1046,13 +987,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
}
|
||||
}
|
||||
|
||||
bool anyAuthenticated;
|
||||
lock (_listenersLock)
|
||||
{
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||
bool anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||
|| DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated;
|
||||
}
|
||||
|
||||
if (anyAuthenticated)
|
||||
{
|
||||
// If we reconnected a authenticated connection we need to re-authenticate
|
||||
@ -1075,10 +1011,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket not connected"));
|
||||
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
|
||||
var subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
if (subList.Count == 0)
|
||||
break;
|
||||
|
||||
@ -1260,6 +1193,37 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
});
|
||||
}
|
||||
|
||||
private void AddMessageProcessor(IMessageProcessor processor)
|
||||
{
|
||||
lock (_listenersLock)
|
||||
{
|
||||
var updatedList = new List<IMessageProcessor>(_listeners);
|
||||
updatedList.Add(processor);
|
||||
_listeners = updatedList.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveMessageProcessor(IMessageProcessor processor)
|
||||
{
|
||||
lock (_listenersLock)
|
||||
{
|
||||
var updatedList = new List<IMessageProcessor>(_listeners);
|
||||
updatedList.Remove(processor);
|
||||
_listeners = updatedList.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveMessageProcessors(IEnumerable<IMessageProcessor> processors)
|
||||
{
|
||||
lock (_listenersLock)
|
||||
{
|
||||
var updatedList = new List<IMessageProcessor>(_listeners);
|
||||
foreach (var processor in processors)
|
||||
updatedList.Remove(processor);
|
||||
_listeners = updatedList.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Testing
|
||||
if (message.Contains("Cannot map"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
if (message.Contains("Received null enum value"))
|
||||
if (message.Contains("Received null or empty enum value"))
|
||||
throw new Exception("Enum null error: " + message);
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ namespace CryptoExchange.Net.Testing
|
||||
if (message.Contains("Cannot map"))
|
||||
throw new Exception("Enum value error: " + message);
|
||||
|
||||
if (message.Contains("Received null enum value"))
|
||||
if (message.Contains("Received null or empty enum value"))
|
||||
throw new Exception("Enum null error: " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
Content = Encoding.UTF8.GetString(data);
|
||||
}
|
||||
|
||||
public void SetContent(string data, string contentType)
|
||||
public void SetContent(string data, Encoding? encoding, string contentType)
|
||||
{
|
||||
Content = data;
|
||||
}
|
||||
|
||||
@ -112,6 +112,8 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
|
||||
public async Task ReconnectAsync()
|
||||
{
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
|
||||
if (OnReconnecting != null)
|
||||
await OnReconnecting().ConfigureAwait(false);
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
@ -59,7 +61,17 @@ namespace CryptoExchange.Net.Testing
|
||||
/// <typeparam name="T">Type of response</typeparam>
|
||||
/// <param name="expression">The call expression</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())
|
||||
return;
|
||||
@ -93,6 +105,22 @@ namespace CryptoExchange.Net.Testing
|
||||
if (!result.Success)
|
||||
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}");
|
||||
}
|
||||
|
||||
|
||||
126
CryptoExchange.Net/Testing/SharedRestRequestValidator.cs
Normal file
126
CryptoExchange.Net/Testing/SharedRestRequestValidator.cs
Normal file
@ -0,0 +1,126 @@
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
/// <summary>
|
||||
/// Validator for REST requests, comparing path, http method, authentication and response parsing
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient">The Rest client</typeparam>
|
||||
public class SharedRestRequestValidator<TClient> where TClient : BaseRestClient
|
||||
{
|
||||
private readonly TClient _client;
|
||||
private readonly Func<WebCallResult, bool> _isAuthenticated;
|
||||
private readonly string _folder;
|
||||
private readonly string _baseAddress;
|
||||
private readonly string? _nestedPropertyForCompare;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="client">Client to test</param>
|
||||
/// <param name="folder">Folder for json test values</param>
|
||||
/// <param name="baseAddress">The base address that is expected</param>
|
||||
/// <param name="isAuthenticated">Func for checking if the request is authenticated</param>
|
||||
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||
public SharedRestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null)
|
||||
{
|
||||
_client = client;
|
||||
_folder = folder;
|
||||
_baseAddress = baseAddress;
|
||||
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||
_isAuthenticated = isAuthenticated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||
/// <param name="methodInvoke">Method invocation</param>
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="endpointOptions">Request options</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public Task ValidateAsync<TResponse>(
|
||||
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
|
||||
string name,
|
||||
EndpointOptions endpointOptions,
|
||||
params Func<TResponse, bool>[] validation)
|
||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, endpointOptions, validation);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||
/// <typeparam name="TActualResponse">The concrete response type</typeparam>
|
||||
/// <param name="methodInvoke">Method invocation</param>
|
||||
/// <param name="name">Method name for looking up json test values</param>
|
||||
/// <param name="endpointOptions">Request options</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task ValidateAsync<TResponse, TActualResponse>(
|
||||
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
|
||||
string name,
|
||||
EndpointOptions endpointOptions,
|
||||
params Func<TResponse, bool>[] validation) where TActualResponse : TResponse
|
||||
{
|
||||
var listener = new EnumValueTraceListener();
|
||||
Trace.Listeners.Add(listener);
|
||||
|
||||
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||
FileStream file;
|
||||
try
|
||||
{
|
||||
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new Exception($"Response file not found for {name}: {path}");
|
||||
}
|
||||
|
||||
var buffer = new byte[file.Length];
|
||||
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
file.Close();
|
||||
|
||||
var data = Encoding.UTF8.GetString(buffer);
|
||||
using var reader = new StringReader(data);
|
||||
var expectedMethod = reader.ReadLine();
|
||||
var expectedPath = reader.ReadLine();
|
||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||
var response = reader.ReadToEnd();
|
||||
|
||||
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||
|
||||
// Check request/response properties
|
||||
if (result.Error != null)
|
||||
throw new Exception(name + " returned error " + result.Error);
|
||||
if (endpointOptions.NeedsAuthentication != expectedAuth)
|
||||
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
|
||||
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||
|
||||
var index = 0;
|
||||
foreach(var validate in validation)
|
||||
{
|
||||
if (!validate(result.Data!))
|
||||
throw new Exception(name + $" response validation #{index} failed");
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
Trace.Listeners.Remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -332,7 +332,8 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
_data.Add(item.OpenTime, item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(v => v.Key);
|
||||
_firstTimestamp = _data.Count == 0 ? null : _data.Min(v => v.Key);
|
||||
|
||||
ApplyWindow(false);
|
||||
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
||||
}
|
||||
@ -375,7 +376,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
}
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(x => x.Key);
|
||||
_firstTimestamp = _data.Count == 0 ? null : _data.Min(x => x.Key);
|
||||
_changed = true;
|
||||
|
||||
SetSyncStatus();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user