mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-07 10:11:10 +00:00
Compare commits
No commits in common. "master" and "CryptoExchange.Net.10.6.2" have entirely different histories.
master
...
CryptoExch
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -30,13 +29,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// act
|
// act
|
||||||
// assert
|
// assert
|
||||||
Assert.Throws(typeof(ArgumentException),
|
Assert.Throws(typeof(ArgumentException),
|
||||||
() => {
|
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||||
var opts = new RestExchangeOptions<TestEnvironment, HMACCredential>()
|
|
||||||
{
|
|
||||||
ApiCredentials = new HMACCredential(key, secret)
|
|
||||||
};
|
|
||||||
opts.ApiCredentials.Validate();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -45,7 +38,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange, act
|
// arrange, act
|
||||||
var options = new TestClientOptions
|
var options = new TestClientOptions
|
||||||
{
|
{
|
||||||
ApiCredentials = new HMACCredential("123", "456"),
|
ApiCredentials = new ApiCredentials("123", "456"),
|
||||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
ReceiveWindow = TimeSpan.FromSeconds(10)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,65 +49,83 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSetOptionsRest()
|
public void TestApiOptionsAreSet()
|
||||||
{
|
{
|
||||||
var client = new TestRestClient();
|
// arrange, act
|
||||||
client.SetOptions(new UpdateOptions
|
var options = new TestClientOptions();
|
||||||
{
|
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||||
RequestTimeout = TimeSpan.FromSeconds(2),
|
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||||
Proxy = new ApiProxy("http://testproxy", 1234)
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
|
// assert
|
||||||
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
|
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||||
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
|
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||||
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||||
|
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSetOptionsRestWithCredentials()
|
public void TestClientUsesCorrectOptions()
|
||||||
{
|
{
|
||||||
var client = new TestRestClient();
|
var client = new TestRestClient(options => {
|
||||||
client.SetOptions(new UpdateOptions<HMACCredential>
|
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||||
{
|
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||||
ApiCredentials = new HMACCredential("123", "456"),
|
|
||||||
RequestTimeout = TimeSpan.FromSeconds(2),
|
|
||||||
Proxy = new ApiProxy("http://testproxy", 1234)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.That(client.Api1.ApiCredentials, Is.Not.Null);
|
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||||
Assert.That(client.Api1.ApiCredentials.Key, Is.EqualTo("123"));
|
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||||
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null);
|
Assert.That(authProvider1.GetKey() == "111");
|
||||||
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy"));
|
Assert.That(authProvider1.GetSecret() == "222");
|
||||||
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
|
Assert.That(authProvider2.GetKey() == "333");
|
||||||
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
Assert.That(authProvider2.GetSecret() == "444");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
|
public void TestClientUsesCorrectOptionsWithDefault()
|
||||||
{
|
{
|
||||||
TestClientOptions.Default = new TestClientOptions
|
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||||
|
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||||
|
|
||||||
|
var client = new TestRestClient();
|
||||||
|
|
||||||
|
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||||
|
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||||
|
Assert.That(authProvider1.GetKey() == "111");
|
||||||
|
Assert.That(authProvider1.GetSecret() == "222");
|
||||||
|
Assert.That(authProvider2.GetKey() == "123");
|
||||||
|
Assert.That(authProvider2.GetSecret() == "456");
|
||||||
|
|
||||||
|
// Cleanup static values
|
||||||
|
TestClientOptions.Default.ApiCredentials = null;
|
||||||
|
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||||
{
|
{
|
||||||
ApiCredentials = new HMACCredential("111", "222"),
|
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||||
RequestTimeout = TimeSpan.FromSeconds(1),
|
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||||
};
|
|
||||||
|
|
||||||
var client1 = new TestRestClient();
|
var client = new TestRestClient(options =>
|
||||||
|
{
|
||||||
|
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||||
|
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||||
|
});
|
||||||
|
|
||||||
Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
|
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||||
Assert.That(client1.ClientOptions.ApiCredentials.Key, Is.EqualTo("111"));
|
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");
|
||||||
|
|
||||||
TestClientOptions.Default.ApiCredentials = new HMACCredential("333", "444");
|
// Cleanup static values
|
||||||
TestClientOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
|
TestClientOptions.Default.ApiCredentials = null;
|
||||||
|
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||||
var client2 = new TestRestClient();
|
|
||||||
|
|
||||||
Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
|
|
||||||
Assert.That(client2.ClientOptions.ApiCredentials.Key, Is.EqualTo("333"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
|
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default options for the futures client
|
/// Default options for the futures client
|
||||||
|
|||||||
@ -81,25 +81,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(result.Error is ServerError);
|
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]
|
[TestCase]
|
||||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,15 +1,11 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
using CryptoExchange.Net.Converters;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using CryptoExchange.Net.Objects;
|
using System.Text.Json;
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using CryptoExchange.Net.Testing;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@ -159,22 +155,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("three", TestEnum.Three)]
|
[TestCase("three", TestEnum.Three)]
|
||||||
[TestCase("Four", TestEnum.Four)]
|
[TestCase("Four", TestEnum.Four)]
|
||||||
[TestCase("four", TestEnum.Four)]
|
[TestCase("four", TestEnum.Four)]
|
||||||
[TestCase("Four1", (TestEnum)(-9))]
|
[TestCase("Four1", TestEnum.One)]
|
||||||
[TestCase(null, (TestEnum)(-9))]
|
[TestCase(null, TestEnum.One)]
|
||||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
|
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
|
|
||||||
{
|
|
||||||
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
|
|
||||||
Assert.That((int)output == -99);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", TestEnum.One)]
|
[TestCase("1", TestEnum.One)]
|
||||||
[TestCase("2", TestEnum.Two)]
|
[TestCase("2", TestEnum.Two)]
|
||||||
[TestCase("3", TestEnum.Three)]
|
[TestCase("3", TestEnum.Three)]
|
||||||
@ -189,31 +178,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(result == expected);
|
Assert.That(result == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
|
|
||||||
{
|
|
||||||
LibraryHelpers.StaticLogger = new TraceLogger();
|
|
||||||
var listener = new EnumValueTraceListener();
|
|
||||||
Trace.Listeners.Add(listener);
|
|
||||||
EnumConverter<TestEnum>.Reset();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Assert.Throws<Exception>(() =>
|
|
||||||
{
|
|
||||||
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.DoesNotThrow(() =>
|
|
||||||
{
|
|
||||||
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Trace.Listeners.Remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase("1", true)]
|
[TestCase("1", true)]
|
||||||
[TestCase("true", true)]
|
[TestCase("true", true)]
|
||||||
[TestCase("yes", true)]
|
[TestCase("yes", true)]
|
||||||
@ -461,20 +425,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Four
|
Four
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
|
|
||||||
public enum TestEnum2
|
|
||||||
{
|
|
||||||
[Map("-9")]
|
|
||||||
Minus9 = -9,
|
|
||||||
[Map("1")]
|
|
||||||
One,
|
|
||||||
[Map("2")]
|
|
||||||
Two,
|
|
||||||
[Map("three", "3")]
|
|
||||||
Three,
|
|
||||||
Four
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonSerializable(typeof(Test))]
|
[JsonSerializable(typeof(Test))]
|
||||||
[JsonSerializable(typeof(Test2))]
|
[JsonSerializable(typeof(Test2))]
|
||||||
[JsonSerializable(typeof(Test3))]
|
[JsonSerializable(typeof(Test3))]
|
||||||
|
|||||||
@ -42,11 +42,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestSubClient : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
public class TestSubClient : RestApiClient
|
||||||
{
|
{
|
||||||
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
||||||
|
|
||||||
public TestSubClient(RestExchangeOptions<TestEnvironment, HMACCredential> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,13 +58,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials) => throw new NotImplementedException();
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestAuthProvider : AuthenticationProvider<HMACCredential, HMACCredential>
|
public class TestAuthProvider : AuthenticationProvider
|
||||||
{
|
{
|
||||||
public TestAuthProvider(HMACCredential credentials) : base(credentials, credentials)
|
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
|
||||||
|
|
||||||
|
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +74,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKey() => Credential.Key;
|
public string GetKey() => _credentials.Key;
|
||||||
public string GetSecret() => Credential.Secret;
|
public string GetSecret() => _credentials.Secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestEnvironment : TradeEnvironment
|
public class TestEnvironment : TradeEnvironment
|
||||||
|
|||||||
@ -23,7 +23,7 @@ using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
public class TestRestClient: BaseRestClient<TestEnvironment, HMACCredential>
|
public class TestRestClient: BaseRestClient
|
||||||
{
|
{
|
||||||
public TestRestApi1Client Api1 { get; }
|
public TestRestApi1Client Api1 { get; }
|
||||||
public TestRestApi2Client Api2 { get; }
|
public TestRestApi2Client Api2 { get; }
|
||||||
@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
Initialize(options.Value);
|
Initialize(options.Value);
|
||||||
|
|
||||||
Api1 = AddApiClient(new TestRestApi1Client(options.Value));
|
Api1 = new TestRestApi1Client(options.Value);
|
||||||
Api2 = AddApiClient(new TestRestApi2Client(options.Value));
|
Api2 = new TestRestApi2Client(options.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetResponse(string responseData, out IRequest requestObj)
|
public void SetResponse(string responseData, out IRequest requestObj)
|
||||||
@ -56,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<Encoding>(), It.IsAny<string>())).Callback(new Action<string, Encoding, string>((content, encoding, type) => { request.Setup(r => r.Content).Returns(content); }));
|
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.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi1Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
public class TestRestApi1Client : RestApiClient
|
||||||
{
|
{
|
||||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
ParameterPositions[method] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||||
@ -168,7 +168,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
|
public class TestRestApi2Client : RestApiClient
|
||||||
{
|
{
|
||||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||||
|
|||||||
@ -19,14 +19,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||||
|
|
||||||
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||||
{
|
{
|
||||||
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
|
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
||||||
if (result.Item1 != null)
|
|
||||||
return result.Item1;
|
|
||||||
|
|
||||||
var errorData = result.Item2.Deserialize<TestError>();
|
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
||||||
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,101 @@
|
|||||||
namespace CryptoExchange.Net.Authentication
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api credentials, used to sign requests accessing private endpoints
|
/// Api credentials, used to sign requests accessing private endpoints
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ApiCredentials
|
public class ApiCredentials
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate the API credentials
|
/// The api key / label to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract void Validate();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the credentials
|
/// Copy the credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public abstract ApiCredentials Copy();
|
public virtual ApiCredentials Copy()
|
||||||
|
{
|
||||||
|
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
25
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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,7 +13,6 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.Default;
|
using CryptoExchange.Net.Sockets.Default;
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication
|
namespace CryptoExchange.Net.Authentication
|
||||||
{
|
{
|
||||||
@ -25,9 +24,58 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The public identifier for the provided credentials
|
/// The supported credential types
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract string Key { get; }
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticate a REST request
|
/// Authenticate a REST request
|
||||||
@ -228,15 +276,21 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA256 sign the data and return the hash
|
/// HMACSHA256 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignHMACSHA256(HMACCredential credential, string data, SignOutputType? outputType = null)
|
/// <param name="data">Data to sign</param>
|
||||||
=> SignHMACSHA256(credential,Encoding.UTF8.GetBytes(data), outputType);
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA256 sign the data and return the hash
|
/// HMACSHA256 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignHMACSHA256(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
using var encryptor = new HMACSHA256(credential.GetSBytes());
|
using var encryptor = new HMACSHA256(_sBytes);
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
@ -244,15 +298,21 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA384 sign the data and return the hash
|
/// HMACSHA384 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignHMACSHA384(HMACCredential credential, string data, SignOutputType? outputType = null)
|
/// <param name="data">Data to sign</param>
|
||||||
=> SignHMACSHA384(credential, Encoding.UTF8.GetBytes(data), outputType);
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA384 sign the data and return the hash
|
/// HMACSHA384 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignHMACSHA384(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
using var encryptor = new HMACSHA384(credential.GetSBytes());
|
using var encryptor = new HMACSHA384(_sBytes);
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
@ -260,15 +320,21 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA512 sign the data and return the hash
|
/// HMACSHA512 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignHMACSHA512(HMACCredential credential, string data, SignOutputType? outputType = null)
|
/// <param name="data">Data to sign</param>
|
||||||
=> SignHMACSHA512(credential, Encoding.UTF8.GetBytes(data), outputType);
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
||||||
|
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HMACSHA512 sign the data and return the hash
|
/// HMACSHA512 sign the data and return the hash
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignHMACSHA512(HMACCredential credential, byte[] data, SignOutputType? outputType = null)
|
/// <param name="data">Data to sign</param>
|
||||||
|
/// <param name="outputType">String type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
using var encryptor = new HMACSHA512(credential.GetSBytes());
|
using var encryptor = new HMACSHA512(_sBytes);
|
||||||
var resultBytes = encryptor.ComputeHash(data);
|
var resultBytes = encryptor.ComputeHash(data);
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
@ -276,21 +342,27 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA256 sign the data
|
/// SHA256 sign the data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignRSASHA256(RSACredential credential, byte[] data, SignOutputType? outputType = null)
|
/// <param name="data"></param>
|
||||||
|
/// <param name="outputType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
var rsa = credential.GetSigner();
|
using var rsa = CreateRSA();
|
||||||
using var sha256 = SHA256.Create();
|
using var sha256 = SHA256.Create();
|
||||||
var hash = sha256.ComputeHash(data);
|
var hash = sha256.ComputeHash(data);
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA384 sign the data
|
/// SHA384 sign the data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignRSASHA384(RSACredential credential, byte[] data, SignOutputType? outputType = null)
|
/// <param name="data"></param>
|
||||||
|
/// <param name="outputType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
var rsa = credential.GetSigner();
|
using var rsa = CreateRSA();
|
||||||
using var sha384 = SHA384.Create();
|
using var sha384 = SHA384.Create();
|
||||||
var hash = sha384.ComputeHash(data);
|
var hash = sha384.ComputeHash(data);
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||||
@ -300,32 +372,79 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// SHA512 sign the data
|
/// SHA512 sign the data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string SignRSASHA512(RSACredential credential, byte[] data, SignOutputType? outputType = null)
|
/// <param name="data"></param>
|
||||||
|
/// <param name="outputType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
var rsa = credential.GetSigner();
|
using var rsa = CreateRSA();
|
||||||
using var sha512 = SHA512.Create();
|
using var sha512 = SHA512.Create();
|
||||||
var hash = sha512.ComputeHash(data);
|
var hash = sha512.ComputeHash(data);
|
||||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ed25519 sign the data
|
/// Ed25519 sign the data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SignEd25519(Ed25519Credential credential, string data, SignOutputType? outputType = null)
|
public string SignEd25519(string data, SignOutputType? outputType = null)
|
||||||
=> SignEd25519(credential, Encoding.ASCII.GetBytes(data), outputType);
|
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ed25519 sign the data
|
/// Ed25519 sign the data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SignEd25519(Ed25519Credential credential, byte[] data, SignOutputType? outputType = null)
|
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
|
||||||
{
|
{
|
||||||
var signKey = credential.GetSigningKey();
|
#if NET8_0_OR_GREATER
|
||||||
var resultBytes = SignatureAlgorithm.Ed25519.Sign(signKey, data);
|
if (Ed25519Key == null)
|
||||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
{
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
|
||||||
|
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||||
|
#else
|
||||||
|
throw new InvalidOperationException();
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private RSA CreateRSA()
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert byte array to hex string
|
/// Convert byte array to hex string
|
||||||
@ -452,179 +571,17 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider
|
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||||
where TApiCredentials: ApiCredentials
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// API credentials used for signing requests
|
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||||
/// </summary>
|
|
||||||
public TApiCredentials ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected AuthenticationProvider(TApiCredentials credentials)
|
/// <param name="credentials"></param>
|
||||||
|
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||||
{
|
{
|
||||||
credentials.Validate();
|
|
||||||
|
|
||||||
ApiCredentials = credentials;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class AuthenticationProvider<TApiCredentials, TCredentialType> : AuthenticationProvider<TApiCredentials>
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
where TCredentialType : CredentialSet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The specific credential type used for signing requests.
|
|
||||||
/// </summary>
|
|
||||||
public TCredentialType Credential { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string Key => Credential.Key;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected AuthenticationProvider(
|
|
||||||
TApiCredentials credentials,
|
|
||||||
TCredentialType? credential) : base(credentials)
|
|
||||||
{
|
|
||||||
if (credential == null)
|
|
||||||
throw new ArgumentException($"Missing \"{typeof(TCredentialType).Name}\" credentials on \"{credentials.GetType().Name}\"");
|
|
||||||
|
|
||||||
Credential = credential;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA256 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA256 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not HMACCredential hmacCredential)
|
|
||||||
throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
|
|
||||||
|
|
||||||
return SignHMACSHA256(hmacCredential, data, outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA384 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not HMACCredential hmacCredential)
|
|
||||||
throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
|
|
||||||
|
|
||||||
return SignHMACSHA384(hmacCredential, data, outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMACSHA512 sign the data and return the hash
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">Data to sign</param>
|
|
||||||
/// <param name="outputType">String type</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not HMACCredential hmacCredential)
|
|
||||||
throw new InvalidOperationException($"Invalid HMAC signing without HMAC credentials provided");
|
|
||||||
|
|
||||||
return SignHMACSHA512(hmacCredential, data, outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA256 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not RSACredential rsaCredential)
|
|
||||||
throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
|
|
||||||
|
|
||||||
return SignRSASHA256(rsaCredential, data, outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA384 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not RSACredential rsaCredential)
|
|
||||||
throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
|
|
||||||
|
|
||||||
return SignRSASHA384(rsaCredential, data, outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SHA512 sign the data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="outputType"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not RSACredential rsaCredential)
|
|
||||||
throw new InvalidOperationException($"Invalid RSA signing without RSA credentials provided");
|
|
||||||
|
|
||||||
return SignRSASHA512(rsaCredential, data, outputType);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if NET8_0_OR_GREATER
|
|
||||||
/// <summary>
|
|
||||||
/// Ed25519 sign the data
|
|
||||||
/// </summary>
|
|
||||||
public string SignEd25519(string data, SignOutputType? outputType = null)
|
|
||||||
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ed25519 sign the data
|
|
||||||
/// </summary>
|
|
||||||
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
|
|
||||||
{
|
|
||||||
if (Credential is not Ed25519Credential ed25519Credential)
|
|
||||||
throw new InvalidOperationException($"Invalid Ed25519 signing without Ed25519 credentials provided");
|
|
||||||
|
|
||||||
return SignEd25519(ed25519Credential, data, outputType);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,569 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,304 +0,0 @@
|
|||||||
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; } = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,385 +0,0 @@
|
|||||||
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,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces.Clients;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
@ -27,11 +28,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
protected bool _disposing;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether a proxy is configured
|
|
||||||
/// </summary>
|
|
||||||
protected bool _proxyConfigured;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Name of the client
|
/// Name of the client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -47,6 +43,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
|
/// </summary>
|
||||||
|
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The environment this client communicates to
|
/// The environment this client communicates to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -57,6 +58,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Authenticated => ApiCredentials != null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Api options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -78,14 +85,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="logger">Logger</param>
|
/// <param name="logger">Logger</param>
|
||||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
|
/// <param name="apiCredentials">Api credentials</param>
|
||||||
/// <param name="clientOptions">Client options</param>
|
/// <param name="clientOptions">Client options</param>
|
||||||
/// <param name="apiOptions">Api options</param>
|
/// <param name="apiOptions">Api options</param>
|
||||||
protected BaseApiClient(
|
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||||
ILogger logger,
|
|
||||||
bool outputOriginalData,
|
|
||||||
string baseAddress,
|
|
||||||
ExchangeOptions clientOptions,
|
|
||||||
ApiOptions apiOptions)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@ -93,10 +96,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ApiOptions = apiOptions;
|
ApiOptions = apiOptions;
|
||||||
OutputOriginalData = outputOriginalData;
|
OutputOriginalData = outputOriginalData;
|
||||||
BaseAddress = baseAddress;
|
BaseAddress = baseAddress;
|
||||||
|
ApiCredentials = apiCredentials?.Copy();
|
||||||
|
|
||||||
_proxyConfigured = ClientOptions.Proxy != null;
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||||
|
|
||||||
@ -110,6 +122,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
ApiCredentials = credentials?.Copy();
|
||||||
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
ClientOptions.Proxy = options.Proxy;
|
||||||
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
|
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -92,6 +92,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">The credentials to set</param>
|
||||||
|
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register an API client
|
/// Register an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces.Clients;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@ -14,11 +10,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Api clients in this client
|
|
||||||
/// </summary>
|
|
||||||
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
|
|
||||||
@ -33,59 +24,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update options
|
|
||||||
/// </summary>
|
|
||||||
public virtual void SetOptions(UpdateOptions options)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetOptions(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class BaseRestClient<TEnvironment, TApiCredentials> : BaseRestClient, IRestClient<TApiCredentials>
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Api clients in this client
|
|
||||||
/// </summary>
|
|
||||||
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provided client options
|
|
||||||
/// </summary>
|
|
||||||
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
|
||||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
public void SetApiCredentials(TApiCredentials credentials)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update options
|
|
||||||
/// </summary>
|
|
||||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetOptions(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using CryptoExchange.Net.Interfaces.Clients;
|
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -21,11 +19,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
#region fields
|
#region fields
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Api clients in this client
|
|
||||||
/// </summary>
|
|
||||||
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -140,58 +133,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update options
|
|
||||||
/// </summary>
|
|
||||||
public virtual void SetOptions(UpdateOptions options)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetOptions(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class BaseSocketClient<TEnvironment, TApiCredentials> : BaseSocketClient, ISocketClient<TApiCredentials>
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Api clients in this client
|
|
||||||
/// </summary>
|
|
||||||
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provided client options
|
|
||||||
/// </summary>
|
|
||||||
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
|
||||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update options
|
|
||||||
/// </summary>
|
|
||||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetOptions(options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
CryptoExchange.Net/Clients/CryptoBaseClient.cs
Normal file
67
CryptoExchange.Net/Clients/CryptoBaseClient.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
CryptoExchange.Net/Clients/CryptoRestClient.cs
Normal file
24
CryptoExchange.Net/Clients/CryptoRestClient.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
CryptoExchange.Net/Clients/CryptoSocketClient.cs
Normal file
24
CryptoExchange.Net/Clients/CryptoSocketClient.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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,4 +1,3 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Caching;
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
@ -78,16 +77,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Encoding/charset for the ContentType header
|
|
||||||
/// </summary>
|
|
||||||
protected Encoding? RequestBodyContentEncoding { get; set; } = Encoding.UTF8;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to omit the ContentType header if there is no content
|
|
||||||
/// </summary>
|
|
||||||
protected bool OmitContentTypeHeaderWithoutContent { get; set; } = false;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||||
|
|
||||||
@ -104,16 +93,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract IRestMessageHandler MessageHandler { get; }
|
protected abstract IRestMessageHandler MessageHandler { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
|
|
||||||
/// </summary>
|
|
||||||
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configured environment name
|
|
||||||
/// </summary>
|
|
||||||
public abstract string EnvironmentName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -122,13 +101,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="options">The base client options</param>
|
/// <param name="options">The base client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public RestApiClient(ILogger logger,
|
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||||
HttpClient? httpClient,
|
|
||||||
string baseAddress,
|
|
||||||
RestExchangeOptions options,
|
|
||||||
RestApiOptions apiOptions)
|
|
||||||
: base(logger,
|
: base(logger,
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||||
|
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
@ -228,7 +204,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
string? rateLimitKeySuffix = null)
|
string? rateLimitKeySuffix = null)
|
||||||
{
|
{
|
||||||
var requestId = ExchangeHelpers.NextId();
|
var requestId = ExchangeHelpers.NextId();
|
||||||
if (definition.Authenticated && GetAuthenticationProvider() == null)
|
if (definition.Authenticated && AuthenticationProvider == null)
|
||||||
{
|
{
|
||||||
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
||||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
return new WebCallResult<T>(new NoApiCredentialsError());
|
||||||
@ -333,17 +309,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await definition.RateLimitGate.ProcessAsync(
|
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
||||||
_logger,
|
|
||||||
requestId,
|
|
||||||
RateLimitItemType.Request,
|
|
||||||
definition,
|
|
||||||
host,
|
|
||||||
GetAuthenticationProvider()?.Key,
|
|
||||||
requestWeight,
|
|
||||||
ClientOptions.RateLimitingBehaviour,
|
|
||||||
rateLimitKeySuffix,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return limitResult.Error!;
|
return limitResult.Error!;
|
||||||
}
|
}
|
||||||
@ -358,18 +324,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var singleRequestWeight = weightSingleLimiter ?? 1;
|
var singleRequestWeight = weightSingleLimiter ?? 1;
|
||||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
|
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
||||||
_logger,
|
|
||||||
requestId,
|
|
||||||
definition.LimitGuard,
|
|
||||||
RateLimitItemType.Request,
|
|
||||||
definition,
|
|
||||||
host,
|
|
||||||
GetAuthenticationProvider()?.Key,
|
|
||||||
singleRequestWeight,
|
|
||||||
ClientOptions.RateLimitingBehaviour,
|
|
||||||
rateLimitKeySuffix,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return limitResult.Error!;
|
return limitResult.Error!;
|
||||||
}
|
}
|
||||||
@ -408,7 +363,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
GetAuthenticationProvider()?.ProcessRequest(this, requestConfiguration);
|
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -419,11 +374,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||||
queryString = $"?{queryString}";
|
queryString = $"?{queryString}";
|
||||||
|
|
||||||
var path = baseAddress.AppendPath(definition.Path);
|
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||||
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
|
|
||||||
path += "/";
|
|
||||||
|
|
||||||
var uri = new Uri(path + queryString);
|
|
||||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||||
request.Accept = MessageHandler.AcceptHeader;
|
request.Accept = MessageHandler.AcceptHeader;
|
||||||
|
|
||||||
@ -447,14 +398,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var bodyContent = requestConfiguration.GetBodyContent();
|
var bodyContent = requestConfiguration.GetBodyContent();
|
||||||
if (bodyContent != null)
|
if (bodyContent != null)
|
||||||
{
|
{
|
||||||
request.SetContent(bodyContent, RequestBodyContentEncoding, contentType);
|
request.SetContent(bodyContent, contentType);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
||||||
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||||
else if (OmitContentTypeHeaderWithoutContent != true)
|
else
|
||||||
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,19 +437,23 @@ namespace CryptoExchange.Net.Clients
|
|||||||
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
|
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
string? originalData = null;
|
string? originalData = null;
|
||||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||||
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
|
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
|
||||||
{
|
{
|
||||||
// Create a seekable stream from the response stream if:
|
// If we want to return the original string data from the stream, but still want to process it
|
||||||
// 1. We need to output the original data
|
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
|
||||||
// 2. The message handler requires a seekable stream
|
var memoryStream = new MemoryStream();
|
||||||
// 3. The response indicates error and we want to output (part of) the returned data
|
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||||
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
|
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
|
||||||
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
|
|
||||||
if (outputOriginalData)
|
if (outputOriginalData)
|
||||||
{
|
{
|
||||||
|
memoryStream.Position = 0;
|
||||||
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
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)
|
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||||
@ -524,6 +479,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
error = await MessageHandler.ParseErrorResponse(
|
error = await MessageHandler.ParseErrorResponse(
|
||||||
@ -695,13 +651,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
stringData = stringSerializer.Serialize(value);
|
stringData = stringSerializer.Serialize(value);
|
||||||
else
|
else
|
||||||
stringData = stringSerializer.Serialize(parameters);
|
stringData = stringSerializer.Serialize(parameters);
|
||||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
request.SetContent(stringData, contentType);
|
||||||
}
|
}
|
||||||
else if (contentType == Constants.FormContentHeader)
|
else if (contentType == Constants.FormContentHeader)
|
||||||
{
|
{
|
||||||
// Write the parameters as form data in the body
|
// Write the parameters as form data in the body
|
||||||
var stringData = parameters.ToFormData();
|
var stringData = parameters.ToFormData();
|
||||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
request.SetContent(stringData, contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -724,6 +680,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns>Server time</returns>
|
/// <returns>Server time</returns>
|
||||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void SetOptions<T>(UpdateOptions<T> options)
|
||||||
|
{
|
||||||
|
base.SetOptions(options);
|
||||||
|
|
||||||
|
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
|
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
|
||||||
{
|
{
|
||||||
if (!definition.Authenticated)
|
if (!definition.Authenticated)
|
||||||
@ -805,184 +769,10 @@ 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)
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
=> ClientOptions.CachingEnabled
|
=> ClientOptions.CachingEnabled
|
||||||
&& definition.Method == HttpMethod.Get
|
&& definition.Method == HttpMethod.Get
|
||||||
&& !definition.PreventCaching;
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetOptions(UpdateOptions options)
|
|
||||||
{
|
|
||||||
_proxyConfigured = options.Proxy != null;
|
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class RestApiClient<TEnvironment> : RestApiClient, IRestApiClient
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new RestExchangeOptions<TEnvironment> ClientOptions => (RestExchangeOptions<TEnvironment>)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string EnvironmentName => ClientOptions.Environment.Name;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected RestApiClient(
|
|
||||||
ILogger logger,
|
|
||||||
HttpClient? httpClient,
|
|
||||||
string baseAddress,
|
|
||||||
RestExchangeOptions options,
|
|
||||||
RestApiOptions apiOptions) : base(
|
|
||||||
logger,
|
|
||||||
httpClient,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class RestApiClient<TEnvironment, TApiCredentials> : RestApiClient<TEnvironment>, IRestApiClient<TApiCredentials>
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Authenticated => ApiCredentials != null;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected RestApiClient(
|
|
||||||
ILogger logger,
|
|
||||||
HttpClient? httpClient,
|
|
||||||
string baseAddress,
|
|
||||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
|
||||||
RestApiOptions apiOptions) : base(
|
|
||||||
logger,
|
|
||||||
httpClient,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
|
||||||
ApiCredentials = options.ApiCredentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
|
||||||
{
|
|
||||||
ApiCredentials = (TApiCredentials)credentials.Copy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
|
||||||
{
|
|
||||||
base.SetOptions(options);
|
|
||||||
|
|
||||||
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class RestApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : RestApiClient<TEnvironment, TApiCredentials>
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
|
|
||||||
private bool _authProviderInitialized = false;
|
|
||||||
private TAuthenticationProvider? _authenticationProvider;
|
|
||||||
/// <summary>
|
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
|
||||||
/// </summary>
|
|
||||||
public TAuthenticationProvider? AuthenticationProvider
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!_authProviderInitialized)
|
|
||||||
{
|
|
||||||
if (ApiCredentials != null)
|
|
||||||
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
|
||||||
|
|
||||||
_authProviderInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _authenticationProvider;
|
|
||||||
}
|
|
||||||
internal set => _authenticationProvider = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected RestApiClient(
|
|
||||||
ILogger logger,
|
|
||||||
HttpClient? httpClient,
|
|
||||||
string baseAddress,
|
|
||||||
RestExchangeOptions<TEnvironment, TApiCredentials> options,
|
|
||||||
RestApiOptions apiOptions) : base(
|
|
||||||
logger,
|
|
||||||
httpClient,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void SetApiCredentials(TApiCredentials credentials)
|
|
||||||
{
|
|
||||||
base.SetApiCredentials(credentials);
|
|
||||||
|
|
||||||
AuthenticationProvider = null;
|
|
||||||
_authProviderInitialized = false;
|
|
||||||
ApiCredentials = credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void SetOptions(UpdateOptions<TApiCredentials> options)
|
|
||||||
{
|
|
||||||
base.SetOptions(options);
|
|
||||||
|
|
||||||
if (options.ApiCredentials != null)
|
|
||||||
{
|
|
||||||
AuthenticationProvider = null;
|
|
||||||
_authProviderInitialized = false;
|
|
||||||
ApiCredentials = options.ApiCredentials;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Interfaces.Clients;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
@ -20,7 +19,6 @@ using System;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -143,16 +141,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
|
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnforceSequenceNumbers { get; set; }
|
public bool EnforceSequenceNumbers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
|
|
||||||
/// </summary>
|
|
||||||
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configured environment name
|
|
||||||
/// </summary>
|
|
||||||
public abstract string EnvironmentName { get; }
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -162,13 +150,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="options">Client options</param>
|
/// <param name="options">Client options</param>
|
||||||
/// <param name="baseAddress">Base address for this API client</param>
|
/// <param name="baseAddress">Base address for this API client</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public SocketApiClient(
|
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
||||||
ILogger logger,
|
|
||||||
string baseAddress,
|
|
||||||
SocketExchangeOptions options,
|
|
||||||
SocketApiOptions apiOptions)
|
|
||||||
: base(logger,
|
: base(logger,
|
||||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||||
|
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||||
baseAddress,
|
baseAddress,
|
||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
@ -250,7 +235,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
if (subscription.Authenticated && GetAuthenticationProvider() == null)
|
if (subscription.Authenticated && AuthenticationProvider == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||||
@ -530,7 +515,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||||
{
|
{
|
||||||
if (GetAuthenticationProvider() == null)
|
if (AuthenticationProvider == null)
|
||||||
return new CallResult(new NoApiCredentialsError());
|
return new CallResult(new NoApiCredentialsError());
|
||||||
|
|
||||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||||
@ -561,7 +546,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||||
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
|
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
/// Adds a system subscription. Used for example to reply to ping requests
|
||||||
@ -927,6 +912,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void SetOptions<T>(UpdateOptions<T> options)
|
||||||
|
{
|
||||||
|
var previousProxyIsSet = ClientOptions.Proxy != null;
|
||||||
|
base.SetOptions(options);
|
||||||
|
|
||||||
|
if ((!previousProxyIsSet && options.Proxy == null)
|
||||||
|
|| _socketConnections.IsEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
||||||
|
|
||||||
|
// Update proxy, also triggers reconnect
|
||||||
|
foreach (var connection in _socketConnections)
|
||||||
|
_ = connection.Value.UpdateProxy(options.Proxy);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1035,174 +1039,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType);
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetOptions(UpdateOptions options)
|
|
||||||
{
|
|
||||||
var previousProxyIsSet = _proxyConfigured;
|
|
||||||
|
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
_proxyConfigured = options.Proxy != null;
|
|
||||||
if ((!previousProxyIsSet && options.Proxy == null)
|
|
||||||
|| _socketConnections.IsEmpty)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Reconnecting websockets to apply proxy");
|
|
||||||
|
|
||||||
// Update proxy, also triggers reconnect
|
|
||||||
foreach (var connection in _socketConnections)
|
|
||||||
_ = connection.Value.UpdateProxy(options.Proxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class SocketApiClient<TEnvironment> : SocketApiClient, ISocketApiClient
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new SocketExchangeOptions<TEnvironment> ClientOptions => (SocketExchangeOptions<TEnvironment>)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string EnvironmentName => ClientOptions.Environment.Name;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected SocketApiClient(
|
|
||||||
ILogger logger,
|
|
||||||
string baseAddress,
|
|
||||||
SocketExchangeOptions<TEnvironment> options,
|
|
||||||
SocketApiOptions apiOptions) : base(
|
|
||||||
logger,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class SocketApiClient<TEnvironment, TApiCredentials> : SocketApiClient<TEnvironment>, ISocketApiClient<TApiCredentials>
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Authenticated => ApiCredentials != null;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected SocketApiClient(
|
|
||||||
ILogger logger,
|
|
||||||
string baseAddress,
|
|
||||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
|
||||||
SocketApiOptions apiOptions) : base(
|
|
||||||
logger,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
|
||||||
ApiCredentials = options.ApiCredentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetApiCredentials(TApiCredentials credentials)
|
|
||||||
{
|
|
||||||
ApiCredentials = (TApiCredentials)credentials.Copy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
|
|
||||||
{
|
|
||||||
base.SetOptions(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract class SocketApiClient<TEnvironment, TAuthenticationProvider, TApiCredentials> : SocketApiClient<TEnvironment, TApiCredentials>
|
|
||||||
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
where TEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
|
|
||||||
private bool _authProviderInitialized = false;
|
|
||||||
private TAuthenticationProvider? _authenticationProvider;
|
|
||||||
/// <summary>
|
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
|
||||||
/// </summary>
|
|
||||||
public TAuthenticationProvider? AuthenticationProvider
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!_authProviderInitialized)
|
|
||||||
{
|
|
||||||
if (ApiCredentials != null)
|
|
||||||
_authenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
|
||||||
|
|
||||||
_authProviderInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _authenticationProvider;
|
|
||||||
}
|
|
||||||
internal set => _authenticationProvider = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override AuthenticationProvider? GetAuthenticationProvider() => AuthenticationProvider;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
protected SocketApiClient(
|
|
||||||
ILogger logger,
|
|
||||||
string baseAddress,
|
|
||||||
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
|
|
||||||
SocketApiOptions apiOptions) : base(
|
|
||||||
logger,
|
|
||||||
baseAddress,
|
|
||||||
options,
|
|
||||||
apiOptions)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract TAuthenticationProvider CreateAuthenticationProvider(TApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void SetApiCredentials(TApiCredentials credentials)
|
|
||||||
{
|
|
||||||
AuthenticationProvider = null;
|
|
||||||
_authProviderInitialized = false;
|
|
||||||
ApiCredentials = credentials;
|
|
||||||
|
|
||||||
base.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void SetOptions(UpdateOptions<TApiCredentials> options)
|
|
||||||
{
|
|
||||||
if (options.ApiCredentials != null)
|
|
||||||
{
|
|
||||||
AuthenticationProvider = null;
|
|
||||||
_authProviderInitialized = false;
|
|
||||||
ApiCredentials = options.ApiCredentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.SetOptions(options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,7 +88,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
#endif
|
#endif
|
||||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
|
|
||||||
private static T? _undefinedEnumValue;
|
|
||||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
|
||||||
internal class NullableEnumConverter : JsonConverter<T?>
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
@ -120,39 +119,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyStringOrNull);
|
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||||
if (t != null)
|
if (t == null)
|
||||||
return t.Value;
|
{
|
||||||
|
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
||||||
if (isEmptyStringOrNull && !_unknownValuesWarned.Contains(null))
|
|
||||||
{
|
{
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
// 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");
|
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();
|
return new T(); // return default value
|
||||||
}
|
}
|
||||||
|
|
||||||
private T GetUndefinedEnumValue()
|
|
||||||
{
|
|
||||||
if (_undefinedEnumValue != null)
|
|
||||||
return _undefinedEnumValue.Value;
|
|
||||||
|
|
||||||
var type = typeof(T);
|
|
||||||
if (!Enum.IsDefined(type, -9))
|
|
||||||
_undefinedEnumValue = (T)Enum.ToObject(type, -9);
|
|
||||||
else if (!Enum.IsDefined(type, -99))
|
|
||||||
_undefinedEnumValue = (T)Enum.ToObject(type, -99);
|
|
||||||
else
|
else
|
||||||
_undefinedEnumValue = (T)Enum.ToObject(type, -999);
|
{
|
||||||
|
return t.Value;
|
||||||
return (T)_undefinedEnumValue;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||||
{
|
{
|
||||||
isEmptyStringOrNull = false;
|
isEmptyString = false;
|
||||||
var enumType = typeof(T);
|
var enumType = typeof(T);
|
||||||
if (_mappingToEnum == null)
|
if (_mappingToEnum == null)
|
||||||
CreateMapping();
|
CreateMapping();
|
||||||
@ -168,16 +154,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (stringValue is null)
|
if (stringValue is null)
|
||||||
{
|
|
||||||
isEmptyStringOrNull = true;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue, out var result))
|
if (!GetValue(enumType, stringValue, out var result))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
{
|
{
|
||||||
isEmptyStringOrNull = true;
|
isEmptyString = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -309,12 +292,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Reset()
|
|
||||||
{
|
|
||||||
_undefinedEnumValue = null;
|
|
||||||
_unknownValuesWarned = new ConcurrentBag<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -18,7 +18,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||||
{
|
{
|
||||||
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||||
private const int _errorResponseSnippetLimit = 128;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Empty rate limit error
|
/// Empty rate limit error
|
||||||
@ -81,20 +80,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var errorMsg = "Deserialization failed, invalid JSON";
|
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
||||||
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,14 +165,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return type identifier for non-json messages
|
|
||||||
/// </summary>
|
|
||||||
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
{
|
{
|
||||||
@ -181,12 +173,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
int? arrayIndex = null;
|
int? arrayIndex = null;
|
||||||
|
|
||||||
_searchResult.Clear();
|
_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);
|
var reader = new Utf8JsonReader(data);
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
throw new Exception("Invalid JSON structure");
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // Start array
|
reader.Read(); // Start array
|
||||||
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
reader.Read();
|
reader.Read();
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.EndArray)
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
throw new Exception("Invalid JSON structure");
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // End array
|
reader.Read(); // End array
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
throw new Exception("Invalid JSON structure");
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // Start array
|
reader.Read(); // Start array
|
||||||
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
||||||
@ -24,7 +24,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
reader.Read();
|
reader.Read();
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.EndArray)
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
throw new Exception("Invalid JSON structure");
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // End array
|
reader.Read(); // End array
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>11.0.3</PackageVersion>
|
<PackageVersion>10.6.2</PackageVersion>
|
||||||
<AssemblyVersion>11.0.3</AssemblyVersion>
|
<AssemblyVersion>10.6.2</AssemblyVersion>
|
||||||
<FileVersion>11.0.3</FileVersion>
|
<FileVersion>10.6.2</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ using CryptoExchange.Net.SharedApis;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -311,11 +310,11 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request parameters</param>
|
/// <param name="request">The request parameters</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = new List<T>();
|
var result = new List<T>();
|
||||||
ExchangeWebResult<T[]> batch;
|
ExchangeWebResult<T[]> batch;
|
||||||
PageRequest? nextPageToken = null;
|
INextPageToken? nextPageToken = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||||
@ -324,42 +323,12 @@ namespace CryptoExchange.Net
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
result.AddRange(batch.Data);
|
result.AddRange(batch.Data);
|
||||||
nextPageToken = batch.NextPageRequest;
|
nextPageToken = batch.NextPageToken;
|
||||||
if (nextPageToken == null)
|
if (nextPageToken == null)
|
||||||
break;
|
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>
|
/// <summary>
|
||||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -531,50 +500,5 @@ namespace CryptoExchange.Net
|
|||||||
// Unknown decimal format, return null
|
// Unknown decimal format, return null
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert byte array to hex string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buff"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string BytesToHexString(byte[] buff)
|
|
||||||
=> BytesToHexString(new ArraySegment<byte>(buff));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert byte array to hex string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="buff"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string BytesToHexString(ArraySegment<byte> buff)
|
|
||||||
{
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
return Convert.ToHexString(buff);
|
|
||||||
#else
|
|
||||||
var result = string.Empty;
|
|
||||||
foreach (var t in buff)
|
|
||||||
result += t.ToString("X2");
|
|
||||||
return result;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a hex encoded string to byte array
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hexString"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static byte[] HexToBytesString(string hexString)
|
|
||||||
{
|
|
||||||
if (hexString.StartsWith("0x"))
|
|
||||||
hexString = hexString.Substring(2);
|
|
||||||
|
|
||||||
byte[] bytes = new byte[hexString.Length / 2];
|
|
||||||
for (int i = 0; i < hexString.Length; i += 2)
|
|
||||||
{
|
|
||||||
string hexSubstring = hexString.Substring(i, 2);
|
|
||||||
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@ namespace CryptoExchange.Net
|
|||||||
if (serializationType == ArrayParametersSerialization.Array)
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
{
|
{
|
||||||
bool firstArrayValue = true;
|
bool firstArrayValue = true;
|
||||||
foreach (var entry in (Array)parameter.Value)
|
foreach (var entry in (object[])parameter.Value)
|
||||||
{
|
{
|
||||||
if (!firstArrayValue)
|
if (!firstArrayValue)
|
||||||
uriString.Append('&');
|
uriString.Append('&');
|
||||||
@ -92,7 +92,7 @@ namespace CryptoExchange.Net
|
|||||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||||
{
|
{
|
||||||
bool firstArrayValue = true;
|
bool firstArrayValue = true;
|
||||||
foreach (var entry in (Array)parameter.Value)
|
foreach (var entry in (object[])parameter.Value)
|
||||||
{
|
{
|
||||||
if (!firstArrayValue)
|
if (!firstArrayValue)
|
||||||
uriString.Append('&');
|
uriString.Append('&');
|
||||||
@ -109,7 +109,7 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
uriString.Append('[');
|
uriString.Append('[');
|
||||||
var firstArrayEntry = true;
|
var firstArrayEntry = true;
|
||||||
foreach (var entry in (Array)parameter.Value)
|
foreach (var entry in (object[])parameter.Value)
|
||||||
{
|
{
|
||||||
if (!firstArrayEntry)
|
if (!firstArrayEntry)
|
||||||
uriString.Append(',');
|
uriString.Append(',');
|
||||||
|
|||||||
@ -15,6 +15,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
string BaseAddress { get; }
|
string BaseAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||||
|
/// </summary>
|
||||||
|
bool Authenticated { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Format a base and quote asset to an exchange accepted symbol
|
/// Format a base and quote asset to an exchange accepted symbol
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -24,5 +29,19 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
|
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the API credentials for this API client
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="credentials"></param>
|
||||||
|
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Api credentials type</typeparam>
|
||||||
|
/// <param name="options">Options to set</param>
|
||||||
|
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
CryptoExchange.Net/Interfaces/Clients/ICryptoRestClient.cs
Normal file
17
CryptoExchange.Net/Interfaces/Clients/ICryptoRestClient.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
CryptoExchange.Net/Interfaces/Clients/ICryptoSocketClient.cs
Normal file
17
CryptoExchange.Net/Interfaces/Clients/ICryptoSocketClient.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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,7 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.Clients
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base rest API client
|
/// Base rest API client
|
||||||
@ -18,25 +15,4 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
int TotalRequestsMade { get; set; }
|
int TotalRequestsMade { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public interface IRestApiClient<TApiCredentials> : IRestApiClient
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
|
||||||
/// </summary>
|
|
||||||
bool Authenticated { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this API client
|
|
||||||
/// </summary>
|
|
||||||
void SetApiCredentials(TApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Options to set</param>
|
|
||||||
void SetOptions(UpdateOptions<TApiCredentials> options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.Clients
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
@ -7,7 +6,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for rest API implementations
|
/// Base class for rest API implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRestClient : IDisposable
|
public interface IRestClient: IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The options provided for this client
|
/// The options provided for this client
|
||||||
@ -28,27 +27,5 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// Whether client is disposed
|
/// Whether client is disposed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool Disposed { get; }
|
bool Disposed { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update specific options
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
|
||||||
void SetOptions(UpdateOptions options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public interface IRestClient<TApiCredentials> : IRestClient where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
void SetApiCredentials(TApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update specific options
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
|
||||||
void SetOptions(UpdateOptions<TApiCredentials> options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||||
@ -11,7 +10,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Socket API client
|
/// Socket API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISocketApiClient : IBaseApiClient
|
public interface ISocketApiClient: IBaseApiClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current amount of socket connections on the API client
|
/// The current amount of socket connections on the API client
|
||||||
@ -74,25 +73,4 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<CallResult> PrepareConnectionsAsync();
|
Task<CallResult> PrepareConnectionsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public interface ISocketApiClient<TApiCredentials> : ISocketApiClient
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
|
||||||
/// </summary>
|
|
||||||
bool Authenticated { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this API client
|
|
||||||
/// </summary>
|
|
||||||
void SetApiCredentials(TApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Options to set</param>
|
|
||||||
void SetOptions(UpdateOptions<TApiCredentials> options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
|
||||||
@ -9,7 +8,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for socket API implementations
|
/// Base class for socket API implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISocketClient : IDisposable
|
public interface ISocketClient: IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The exchange name
|
/// The exchange name
|
||||||
@ -60,28 +59,5 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UnsubscribeAllAsync();
|
Task UnsubscribeAllAsync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update specific options
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
|
||||||
void SetOptions(UpdateOptions options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public interface ISocketClient<TApiCredentials> : ISocketClient where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
void SetApiCredentials(TApiCredentials credentials);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update specific options
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">Options to update. Only specific options are changeable after the client has been created</param>
|
|
||||||
void SetOptions(UpdateOptions<TApiCredentials> options);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@ -44,7 +43,9 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set string content
|
/// Set string content
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void SetContent(string data, Encoding? encoding, string contentType);
|
/// <param name="data"></param>
|
||||||
|
/// <param name="contentType"></param>
|
||||||
|
void SetContent(string data, string contentType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a header to the request
|
/// Add a header to the request
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -107,36 +105,31 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new HttpMessageHandler instance
|
/// Create a new HttpMessageHandler instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static HttpMessageHandler CreateHttpClientMessageHandler(RestExchangeOptions options)
|
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
|
||||||
{
|
{
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
var socketHandler = new SocketsHttpHandler();
|
var socketHandler = new SocketsHttpHandler();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (options.HttpKeepAliveInterval != null && options.HttpKeepAliveInterval != TimeSpan.Zero)
|
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
||||||
socketHandler.KeepAlivePingDelay = options.HttpKeepAliveInterval.Value;
|
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
|
||||||
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||||
|
|
||||||
socketHandler.EnableMultipleHttp2Connections = options.HttpEnableMultipleHttp2Connections;
|
|
||||||
socketHandler.PooledConnectionLifetime = options.HttpPooledConnectionLifetime;
|
|
||||||
socketHandler.PooledConnectionIdleTimeout = options.HttpPooledConnectionIdleTimeout;
|
|
||||||
socketHandler.MaxConnectionsPerServer = options.HttpMaxConnectionsPerServer;
|
|
||||||
}
|
}
|
||||||
catch (PlatformNotSupportedException) { }
|
catch (PlatformNotSupportedException) { }
|
||||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
if (options.Proxy != null)
|
if (proxy != null)
|
||||||
{
|
{
|
||||||
socketHandler.Proxy = new WebProxy
|
socketHandler.Proxy = new WebProxy
|
||||||
{
|
{
|
||||||
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return socketHandler;
|
return socketHandler;
|
||||||
@ -150,12 +143,12 @@ namespace CryptoExchange.Net
|
|||||||
catch (PlatformNotSupportedException) { }
|
catch (PlatformNotSupportedException) { }
|
||||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
if (options.Proxy != null)
|
if (proxy != null)
|
||||||
{
|
{
|
||||||
httpHandler.Proxy = new WebProxy
|
httpHandler.Proxy = new WebProxy
|
||||||
{
|
{
|
||||||
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return httpHandler;
|
return httpHandler;
|
||||||
|
|||||||
@ -154,9 +154,6 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public CallResult AsDataless()
|
public CallResult AsDataless()
|
||||||
{
|
{
|
||||||
if (Error != null )
|
|
||||||
return new CallResult(Error);
|
|
||||||
|
|
||||||
return SuccessResult;
|
return SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,11 +531,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="exchange">The exchange</param>
|
/// <param name="exchange">The exchange</param>
|
||||||
/// <param name="tradeMode">Trade mode the result applies to</param>
|
/// <param name="tradeMode">Trade mode the result applies to</param>
|
||||||
/// <param name="data">Data</param>
|
/// <param name="data">Data</param>
|
||||||
/// <param name="nextPageRequest">Next page request</param>
|
/// <param name="nextPageToken">Next page token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
|
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -548,11 +545,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="exchange">The exchange</param>
|
/// <param name="exchange">The exchange</param>
|
||||||
/// <param name="tradeModes">Trade modes the result applies to</param>
|
/// <param name="tradeModes">Trade modes the result applies to</param>
|
||||||
/// <param name="data">Data</param>
|
/// <param name="data">Data</param>
|
||||||
/// <param name="nextPageRequest">Next page token</param>
|
/// <param name="nextPageToken">Next page token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
|
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -130,8 +130,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default error info
|
/// Default error info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false,
|
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint");
|
||||||
"No credentials provided for private endpoint, set the `ApiCredentials` option in the client configuration");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -212,15 +211,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DeserializeError(string? message = null, Exception? exception = null)
|
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||||
: base(null,
|
|
||||||
_errorInfo with
|
|
||||||
{
|
|
||||||
Message = message?.Length > 0
|
|
||||||
? message
|
|
||||||
: _errorInfo.Message
|
|
||||||
},
|
|
||||||
exception) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
namespace CryptoExchange.Net.Objects.Options
|
using CryptoExchange.Net.Authentication;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for API usage
|
/// Options for API usage
|
||||||
@ -15,5 +17,10 @@
|
|||||||
/// Note that this comes at a performance cost
|
/// Note that this comes at a performance cost
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? OutputOriginalData { get; set; }
|
public bool? OutputOriginalData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
||||||
|
/// </summary>
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
|
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api credentials used for signing requests to this API.
|
||||||
|
/// </summary>
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not client side rate limiting should be applied
|
/// Whether or not client side rate limiting should be applied
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -40,7 +45,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
|
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,15 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Library options
|
/// Library options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
|
/// <typeparam name="TRestOptions"></typeparam>
|
||||||
where TRestOptions : RestExchangeOptions<TEnvironment>, new()
|
/// <typeparam name="TSocketOptions"></typeparam>
|
||||||
where TSocketOptions : SocketExchangeOptions<TEnvironment>, new()
|
/// <typeparam name="TApiCredentials"></typeparam>
|
||||||
where TEnvironment : TradeEnvironment
|
/// <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
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rest client options
|
/// Rest client options
|
||||||
@ -26,6 +31,11 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TEnvironment? Environment { get; set; }
|
public TEnvironment? Environment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api credentials used for signing requests.
|
||||||
|
/// </summary>
|
||||||
|
public TApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The DI service lifetime for the socket client
|
/// The DI service lifetime for the socket client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -34,8 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy values from these options to the target options
|
/// Copy values from these options to the target options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T Set<T>(T targetOptions) where T : LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
|
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||||
{
|
{
|
||||||
|
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
||||||
targetOptions.Environment = Environment;
|
targetOptions.Environment = Environment;
|
||||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||||
@ -44,29 +55,4 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
return targetOptions;
|
return targetOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Library options
|
|
||||||
/// </summary>
|
|
||||||
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment> : LibraryOptions<TRestOptions, TSocketOptions, TEnvironment>
|
|
||||||
where TRestOptions: RestExchangeOptions<TEnvironment, TApiCredentials>, new()
|
|
||||||
where TSocketOptions: SocketExchangeOptions<TEnvironment, TApiCredentials>, new()
|
|
||||||
where TApiCredentials: ApiCredentials
|
|
||||||
where TEnvironment: TradeEnvironment
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The api credentials used for signing requests.
|
|
||||||
/// </summary>
|
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copy values from these options to the target options
|
|
||||||
/// </summary>
|
|
||||||
public new T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
|
||||||
{
|
|
||||||
targetOptions = base.Set(targetOptions);
|
|
||||||
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
|
||||||
return targetOptions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,10 +18,27 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Set<T>(T item) where T : RestApiOptions, new()
|
public T Set<T>(T item) where T : RestApiOptions, new()
|
||||||
{
|
{
|
||||||
|
item.ApiCredentials = ApiCredentials?.Copy();
|
||||||
item.OutputOriginalData = OutputOriginalData;
|
item.OutputOriginalData = OutputOriginalData;
|
||||||
item.AutoTimestamp = AutoTimestamp;
|
item.AutoTimestamp = AutoTimestamp;
|
||||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http API options
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TApiCredentials"></typeparam>
|
||||||
|
public class RestApiOptions<TApiCredentials>: RestApiOptions where TApiCredentials: ApiCredentials
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The api credentials used for signing requests to this API.
|
||||||
|
/// </summary>
|
||||||
|
public new TApiCredentials? ApiCredentials
|
||||||
|
{
|
||||||
|
get => (TApiCredentials?)base.ApiCredentials;
|
||||||
|
set => base.ApiCredentials = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,8 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for a rest exchange client
|
/// Options for a rest exchange client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RestExchangeOptions : ExchangeOptions
|
public class RestExchangeOptions: ExchangeOptions
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -33,29 +32,10 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
#else
|
#else
|
||||||
= new Version(1, 1);
|
= new Version(1, 1);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
|
/// Http client keep alive interval for keeping connections open
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
|
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>
|
/// <summary>
|
||||||
/// Set the values of this options on the target options
|
/// Set the values of this options on the target options
|
||||||
@ -65,6 +45,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.OutputOriginalData = OutputOriginalData;
|
item.OutputOriginalData = OutputOriginalData;
|
||||||
item.AutoTimestamp = AutoTimestamp;
|
item.AutoTimestamp = AutoTimestamp;
|
||||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||||
|
item.ApiCredentials = ApiCredentials?.Copy();
|
||||||
item.Proxy = Proxy;
|
item.Proxy = Proxy;
|
||||||
item.RequestTimeout = RequestTimeout;
|
item.RequestTimeout = RequestTimeout;
|
||||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||||
@ -73,19 +54,15 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.CachingMaxAge = CachingMaxAge;
|
item.CachingMaxAge = CachingMaxAge;
|
||||||
item.HttpVersion = HttpVersion;
|
item.HttpVersion = HttpVersion;
|
||||||
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
|
|
||||||
item.HttpPooledConnectionLifetime = HttpPooledConnectionLifetime;
|
|
||||||
item.HttpPooledConnectionIdleTimeout = HttpPooledConnectionIdleTimeout;
|
|
||||||
item.HttpEnableMultipleHttp2Connections = HttpEnableMultipleHttp2Connections;
|
|
||||||
#endif
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions
|
/// Options for a rest exchange client
|
||||||
where TEnvironment : TradeEnvironment
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnvironment"></typeparam>
|
||||||
|
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||||
@ -106,32 +83,20 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment>
|
/// Options for a rest exchange client
|
||||||
where TEnvironment : TradeEnvironment
|
/// </summary>
|
||||||
where TApiCredentials : ApiCredentials
|
/// <typeparam name="TEnvironment"></typeparam>
|
||||||
|
/// <typeparam name="TApiCredentials"></typeparam>
|
||||||
|
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api credentials used for signing requests to this API.
|
/// The api credentials used for signing requests to this API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
public new TApiCredentials? ApiCredentials
|
||||||
|
|
||||||
/// <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()
|
|
||||||
{
|
{
|
||||||
base.Set(item);
|
get => (TApiCredentials?)base.ApiCredentials;
|
||||||
item.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
set => base.ApiCredentials = value;
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Set<T>(T item) where T : SocketApiOptions, new()
|
public T Set<T>(T item) where T : SocketApiOptions, new()
|
||||||
{
|
{
|
||||||
|
item.ApiCredentials = ApiCredentials?.Copy();
|
||||||
item.OutputOriginalData = OutputOriginalData;
|
item.OutputOriginalData = OutputOriginalData;
|
||||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||||
item.AutoTimestamp = AutoTimestamp;
|
item.AutoTimestamp = AutoTimestamp;
|
||||||
@ -31,4 +32,20 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Socket API options
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TApiCredentials"></typeparam>
|
||||||
|
public class SocketApiOptions<TApiCredentials> : SocketApiOptions where TApiCredentials : ApiCredentials
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The api credentials used for signing requests to this API.
|
||||||
|
/// </summary>
|
||||||
|
public new TApiCredentials? ApiCredentials
|
||||||
|
{
|
||||||
|
get => (TApiCredentials?)base.ApiCredentials;
|
||||||
|
set => base.ApiCredentials = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,6 +91,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||||
{
|
{
|
||||||
|
item.ApiCredentials = ApiCredentials?.Copy();
|
||||||
item.AutoTimestamp = AutoTimestamp;
|
item.AutoTimestamp = AutoTimestamp;
|
||||||
item.OutputOriginalData = OutputOriginalData;
|
item.OutputOriginalData = OutputOriginalData;
|
||||||
item.ReconnectPolicy = ReconnectPolicy;
|
item.ReconnectPolicy = ReconnectPolicy;
|
||||||
@ -109,11 +110,12 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions
|
/// Options for a socket exchange client
|
||||||
where TEnvironment : TradeEnvironment
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnvironment"></typeparam>
|
||||||
|
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||||
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
/// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||||
@ -136,29 +138,17 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for a socket exchange client
|
/// Options for a socket exchange client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment>
|
/// <typeparam name="TEnvironment"></typeparam>
|
||||||
where TEnvironment : TradeEnvironment
|
/// <typeparam name="TApiCredentials"></typeparam>
|
||||||
where TApiCredentials : ApiCredentials
|
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api credentials used for signing requests to this API.
|
/// The api credentials used for signing requests to this API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
public new TApiCredentials? ApiCredentials
|
||||||
|
|
||||||
/// <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()
|
|
||||||
{
|
{
|
||||||
base.Set(item);
|
get => (TApiCredentials?)base.ApiCredentials;
|
||||||
item.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
set => base.ApiCredentials = value;
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,27 +6,22 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options to update
|
/// Options to update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateOptions
|
public class UpdateOptions<T> where T : ApiCredentials
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
/// Proxy setting. Note that if this is not provided any previously set proxy will be reset
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiProxy? Proxy { get; set; }
|
public ApiProxy? Proxy { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Api credentials
|
||||||
|
/// </summary>
|
||||||
|
public T? ApiCredentials { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Request timeout
|
/// Request timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? RequestTimeout { get; set; }
|
public TimeSpan? RequestTimeout { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Options to update
|
public class UpdateOptions : UpdateOptions<ApiCredentials> { }
|
||||||
/// </summary>
|
|
||||||
public class UpdateOptions<TApiCredentials>: UpdateOptions
|
|
||||||
where TApiCredentials : ApiCredentials
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Api credentials
|
|
||||||
/// </summary>
|
|
||||||
public TApiCredentials? ApiCredentials { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,11 +72,6 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ConnectionId { get; set; }
|
public int? ConnectionId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the endpoint path should always include the trailing `/`
|
|
||||||
/// </summary>
|
|
||||||
public bool? ForcePathEndWithSlash { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -47,7 +47,6 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="preventCaching">Prevent request caching</param>
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@ -60,9 +59,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
bool? preventCaching = null,
|
bool? preventCaching = null,
|
||||||
bool? tryParseOnNonSuccess = null,
|
bool? tryParseOnNonSuccess = null)
|
||||||
bool? forcePathEndWithSlash = null)
|
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
|
||||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||||
@ -79,7 +77,6 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="preventCaching">Prevent request caching</param>
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
string identifier,
|
string identifier,
|
||||||
@ -93,8 +90,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
bool? preventCaching = null,
|
bool? preventCaching = null,
|
||||||
bool? tryParseOnNonSuccess = null,
|
bool? tryParseOnNonSuccess = null)
|
||||||
bool? forcePathEndWithSlash = null)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(identifier, out var def))
|
if (!_definitions.TryGetValue(identifier, out var def))
|
||||||
@ -109,8 +105,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
PreventCaching = preventCaching ?? false,
|
PreventCaching = preventCaching ?? false,
|
||||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
|
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
|
||||||
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
|
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(identifier, def);
|
_definitions.TryAdd(identifier, def);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,11 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
public PathStartFilter(string path)
|
public PathStartFilter(string path)
|
||||||
{
|
{
|
||||||
_path = path.TrimStart('/');
|
_path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
|||||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.RateLimiting.Guards
|
namespace CryptoExchange.Net.RateLimiting.Guards
|
||||||
{
|
{
|
||||||
@ -37,7 +36,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
private readonly double? _decayRate;
|
private readonly double? _decayRate;
|
||||||
private readonly int? _connectionWeight;
|
private readonly int? _connectionWeight;
|
||||||
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||||
private readonly SemaphoreSlim? _sharedGuardSemaphore;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "RateLimitGuard";
|
public string Name => "RateLimitGuard";
|
||||||
@ -54,11 +52,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan TimeSpan { get; }
|
public TimeSpan TimeSpan { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this guard is shared between multiple gates
|
|
||||||
/// </summary>
|
|
||||||
public bool SharedGuard { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -69,9 +62,8 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <param name="windowType">Type of rate limit window</param>
|
/// <param name="windowType">Type of rate limit window</param>
|
||||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||||
/// <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)
|
||||||
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)
|
||||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,27 +77,22 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <param name="windowType">Type of rate limit window</param>
|
/// <param name="windowType">Type of rate limit window</param>
|
||||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||||
/// <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)
|
||||||
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
|
|
||||||
{
|
{
|
||||||
_filters = filters;
|
_filters = filters;
|
||||||
_trackers = new Dictionary<string, IWindowTracker>();
|
_trackers = new Dictionary<string, IWindowTracker>();
|
||||||
_windowType = windowType;
|
_windowType = windowType;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
TimeSpan = timeSpan;
|
TimeSpan = timeSpan;
|
||||||
SharedGuard = shared;
|
|
||||||
_keySelector = keySelector;
|
_keySelector = keySelector;
|
||||||
_decayRate = decayPerTimeSpan;
|
_decayRate = decayPerTimeSpan;
|
||||||
_connectionWeight = connectionWeight;
|
_connectionWeight = connectionWeight;
|
||||||
|
|
||||||
if (SharedGuard)
|
|
||||||
_sharedGuardSemaphore = new SemaphoreSlim(1, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||||
{
|
{
|
||||||
foreach (var filter in _filters)
|
foreach(var filter in _filters)
|
||||||
{
|
{
|
||||||
if (!filter.Passes(type, definition, host, apiKey))
|
if (!filter.Passes(type, definition, host, apiKey))
|
||||||
return LimitCheck.NotApplicable;
|
return LimitCheck.NotApplicable;
|
||||||
@ -114,11 +101,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
if (type == RateLimitItemType.Connection)
|
if (type == RateLimitItemType.Connection)
|
||||||
requestWeight = _connectionWeight ?? requestWeight;
|
requestWeight = _connectionWeight ?? requestWeight;
|
||||||
|
|
||||||
if (SharedGuard)
|
|
||||||
_sharedGuardSemaphore!.Wait();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||||
if (!_trackers.TryGetValue(key, out var tracker))
|
if (!_trackers.TryGetValue(key, out var tracker))
|
||||||
{
|
{
|
||||||
@ -126,19 +108,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
_trackers.Add(key, tracker);
|
_trackers.Add(key, tracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var delay = tracker.GetWaitTime(requestWeight);
|
var delay = tracker.GetWaitTime(requestWeight);
|
||||||
if (delay == default)
|
if (delay == default)
|
||||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||||
|
|
||||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (SharedGuard)
|
|
||||||
_sharedGuardSemaphore!.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
|
||||||
@ -152,23 +127,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
if (type == RateLimitItemType.Connection)
|
if (type == RateLimitItemType.Connection)
|
||||||
requestWeight = _connectionWeight ?? requestWeight;
|
requestWeight = _connectionWeight ?? requestWeight;
|
||||||
|
|
||||||
|
|
||||||
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
var key = _keySelector(definition, host, apiKey) + keySuffix;
|
||||||
var tracker = _trackers[key];
|
var tracker = _trackers[key];
|
||||||
|
|
||||||
if (SharedGuard)
|
|
||||||
_sharedGuardSemaphore!.Wait();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tracker.ApplyWeight(requestWeight);
|
tracker.ApplyWeight(requestWeight);
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (SharedGuard)
|
|
||||||
_sharedGuardSemaphore!.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
|
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create request object for web request
|
/// Create request object for web request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="requestId"></param>
|
||||||
public Request(HttpRequestMessage request, HttpClient client, int requestId)
|
public Request(HttpRequestMessage request, HttpClient client, int requestId)
|
||||||
{
|
{
|
||||||
_httpClient = client;
|
_httpClient = client;
|
||||||
@ -52,12 +55,10 @@ namespace CryptoExchange.Net.Requests
|
|||||||
public int RequestId { get; }
|
public int RequestId { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetContent(string data, Encoding? encoding, string contentType)
|
public void SetContent(string data, string contentType)
|
||||||
{
|
{
|
||||||
Content = data;
|
Content = data;
|
||||||
_request.Content = new StringContent(data, encoding ?? Encoding.UTF8, contentType);
|
_request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||||
if (encoding == null)
|
|
||||||
_request.Content.Headers.ContentType!.CharSet = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
@ -13,16 +12,14 @@ namespace CryptoExchange.Net.Requests
|
|||||||
public class RequestFactory : IRequestFactory
|
public class RequestFactory : IRequestFactory
|
||||||
{
|
{
|
||||||
private HttpClient? _httpClient;
|
private HttpClient? _httpClient;
|
||||||
private RestExchangeOptions? _options;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
||||||
{
|
{
|
||||||
if (client == null)
|
if (client == null)
|
||||||
client = CreateClient(options);
|
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
|
||||||
|
|
||||||
_httpClient = client;
|
_httpClient = client;
|
||||||
_options = options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -42,20 +39,15 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
var newOptions = new RestExchangeOptions();
|
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
|
||||||
_options!.Set(newOptions);
|
|
||||||
newOptions.Proxy = proxy;
|
|
||||||
newOptions.RequestTimeout = requestTimeout;
|
|
||||||
newOptions.HttpKeepAliveInterval = httpKeepAliveInterval;
|
|
||||||
_httpClient = CreateClient(newOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateClient(RestExchangeOptions options)
|
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
var handler = LibraryHelpers.CreateHttpClientMessageHandler(options);
|
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
|
||||||
var client = new HttpClient(handler)
|
var client = new HttpClient(handler)
|
||||||
{
|
{
|
||||||
Timeout = options.RequestTimeout
|
Timeout = requestTimeout
|
||||||
};
|
};
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,11 +16,6 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Order has been canceled
|
/// Order has been canceled
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Canceled,
|
Canceled
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unknown/unmapped status
|
|
||||||
/// </summary>
|
|
||||||
Unknown
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,11 +16,6 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Completed
|
/// Completed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Completed,
|
Completed
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unknown/unmapped status
|
|
||||||
/// </summary>
|
|
||||||
Unknown
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,11 +20,6 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trigger order has been triggered. Resulting order might be filled or not.
|
/// Trigger order has been triggered. Resulting order might be filled or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Triggered,
|
Triggered
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unknown/unmapped status
|
|
||||||
/// </summary>
|
|
||||||
Unknown
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs
Normal file
111
CryptoExchange.Net/SharedApis/Interfaces/INextPageToken.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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
|
/// Get funding rate records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,14 +73,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get closed orders request options
|
/// Spot get closed orders request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
|
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedFuturesOrdersOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get info on closed futures orders
|
/// Get info on closed futures orders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures get order trades request options
|
/// Futures get order trades request options
|
||||||
@ -96,14 +96,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures user trades request options
|
/// Futures user trades request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
GetUserTradesOptions GetFuturesUserTradesOptions { get; }
|
PaginatedEndpointOptions<GetUserTradesRequest> GetFuturesUserTradesOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get futures user trade records
|
/// Get futures user trade records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures cancel order request options
|
/// Futures cancel order request options
|
||||||
|
|||||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get index price kline/candlestick data
|
/// Get index price kline/candlestick data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get mark price kline/candlestick data
|
/// Get mark price kline/candlestick data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get position history
|
/// Get position history
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,9 +30,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get deposit records
|
/// Get deposit records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get kline/candlestick data
|
/// Get kline/candlestick data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get public trade history
|
/// Get public trade history
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get withdrawal records
|
/// Get withdrawal records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,14 +72,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get closed orders request options
|
/// Spot get closed orders request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
GetClosedOrdersOptions GetClosedSpotOrdersOptions { get; }
|
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedSpotOrdersOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get info on closed spot orders
|
/// Get info on closed spot orders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get order trades request options
|
/// Spot get order trades request options
|
||||||
@ -95,14 +95,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot user trades request options
|
/// Spot user trades request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
GetUserTradesOptions GetSpotUserTradesOptions { get; }
|
PaginatedEndpointOptions<GetUserTradesRequest> GetSpotUserTradesOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get spot user trade records
|
/// Get spot user trade records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot cancel order request options
|
/// Spot cancel order request options
|
||||||
|
|||||||
@ -24,9 +24,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public TradingMode[]? DataTradeMode { get; }
|
public TradingMode[]? DataTradeMode { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Next page request, can be passed to the next request on the same endpoint to get the next page
|
/// Token to retrieve the next page with
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PageRequest? NextPageRequest { get; }
|
public INextPageToken? NextPageToken { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -46,7 +46,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
string exchange,
|
string exchange,
|
||||||
TradingMode dataTradeMode,
|
TradingMode dataTradeMode,
|
||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
PageRequest? nextPageToken = null) :
|
INextPageToken? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
result.HttpVersion,
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
DataTradeMode = new[] { dataTradeMode };
|
DataTradeMode = new[] { dataTradeMode };
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
NextPageRequest = nextPageToken;
|
NextPageToken = nextPageToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -74,7 +74,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
string exchange,
|
string exchange,
|
||||||
TradingMode[]? dataTradeModes,
|
TradingMode[]? dataTradeModes,
|
||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
PageRequest? nextPageRequest = null) :
|
INextPageToken? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
result.HttpVersion,
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
@ -92,7 +92,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
DataTradeMode = dataTradeModes;
|
DataTradeMode = dataTradeModes;
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
NextPageRequest = nextPageRequest;
|
NextPageToken = nextPageToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -115,7 +115,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
ResultDataSource dataSource,
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error,
|
Error? error,
|
||||||
PageRequest? nextPageToken = null) : base(
|
INextPageToken? nextPageToken = null) : base(
|
||||||
code,
|
code,
|
||||||
httpVersion,
|
httpVersion,
|
||||||
responseHeaders,
|
responseHeaders,
|
||||||
@ -133,7 +133,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
DataTradeMode = dataTradeModes;
|
DataTradeMode = dataTradeModes;
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
NextPageRequest = nextPageToken;
|
NextPageToken = nextPageToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -144,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
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, NextPageRequest);
|
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@ -9,36 +8,24 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetClosedOrdersOptions : PaginatedEndpointOptions<GetClosedOrdersRequest>
|
public class GetClosedOrdersOptions : PaginatedEndpointOptions<GetClosedOrdersRequest>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the start/end time filter is supported
|
||||||
|
/// </summary>
|
||||||
|
public bool TimeFilterSupported { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetClosedOrdersOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
|
||||||
{
|
{
|
||||||
|
TimeFilterSupported = timeFilterSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
if (!TimeFilterSupported && request.StartTime != null)
|
||||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter 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);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
@ -47,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@ -9,36 +8,24 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetDepositsOptions : PaginatedEndpointOptions<GetDepositsRequest>
|
public class GetDepositsOptions : PaginatedEndpointOptions<GetDepositsRequest>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the start/end time filter is supported
|
||||||
|
/// </summary>
|
||||||
|
public bool TimeFilterSupported { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetDepositsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
|
||||||
{
|
{
|
||||||
|
TimeFilterSupported = timeFilterSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
if (!TimeFilterSupported && request.StartTime != null)
|
||||||
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 ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
@ -47,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
namespace CryptoExchange.Net.SharedApis
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for requesting funding rate history
|
/// Options for requesting funding rate history
|
||||||
@ -12,43 +8,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetFundingRateHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, 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,12 +18,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of data points which can be requested
|
/// Max number of data points which can be requested
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxTotalDataPoints { get; set; }
|
public int? MaxTotalDataPoints { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The max age of the data that can be requested
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? MaxAge { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
|
||||||
{
|
{
|
||||||
SupportIntervals = new[]
|
SupportIntervals = new[]
|
||||||
{
|
{
|
||||||
@ -47,8 +50,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals)
|
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
|
||||||
{
|
{
|
||||||
SupportIntervals = intervals;
|
SupportIntervals = intervals;
|
||||||
}
|
}
|
||||||
@ -66,29 +68,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
if (!IsSupported(request.Interval))
|
if (!IsSupported(request.Interval))
|
||||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported");
|
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))
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
|
||||||
|
|
||||||
if (request.Limit > MaxLimit)
|
if (request.Limit > MaxLimit)
|
||||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request");
|
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 (MaxTotalDataPoints.HasValue)
|
||||||
{
|
{
|
||||||
if (request.Limit > MaxTotalDataPoints.Value)
|
if (request.Limit > MaxTotalDataPoints.Value)
|
||||||
@ -108,7 +93,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
|
||||||
sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}");
|
sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}");
|
||||||
if (MaxAge != null)
|
if (MaxAge != null)
|
||||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
sb.AppendLine($"Max age of data: {MaxAge}");
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
namespace CryptoExchange.Net.SharedApis
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for requesting position history
|
/// Options for requesting position history
|
||||||
@ -12,43 +8,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetPositionHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||||
: 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,27 +9,34 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetTradeHistoryOptions : PaginatedEndpointOptions<GetTradeHistoryRequest>
|
public class GetTradeHistoryOptions : PaginatedEndpointOptions<GetTradeHistoryRequest>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The max age of data that can be requested
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? MaxAge { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetTradeHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
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))
|
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");
|
return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available");
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
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,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@ -9,36 +8,24 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetWithdrawalsOptions : PaginatedEndpointOptions<GetWithdrawalsRequest>
|
public class GetWithdrawalsOptions : PaginatedEndpointOptions<GetWithdrawalsRequest>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the start/end time filter is supported
|
||||||
|
/// </summary>
|
||||||
|
public bool TimeFilterSupported { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetWithdrawalsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||||
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
|
||||||
{
|
{
|
||||||
|
TimeFilterSupported = timeFilterSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
if (!TimeFilterSupported && request.StartTime != null)
|
||||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter 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);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
@ -47,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@ -15,13 +14,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether ascending data retrieval and pagination is available
|
/// Type of pagination supported
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SupportsAscending { get; set; }
|
public SharedPaginationSupport PaginationSupport { get; }
|
||||||
/// <summary>
|
|
||||||
/// Whether ascending data retrieval and pagination is available
|
|
||||||
/// </summary>
|
|
||||||
public bool SupportsDescending { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether filtering based on start/end time is supported
|
/// Whether filtering based on start/end time is supported
|
||||||
@ -33,23 +28,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxLimit { get; set; }
|
public int MaxLimit { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Max age of data that can be requested
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan? MaxAge { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PaginatedEndpointOptions(
|
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
|
||||||
bool supportsAscending,
|
|
||||||
bool supportsDescending,
|
|
||||||
bool timePeriodSupport,
|
|
||||||
int maxLimit,
|
|
||||||
bool needsAuthentication) : base(needsAuthentication)
|
|
||||||
{
|
{
|
||||||
SupportsAscending = supportsAscending;
|
PaginationSupport = paginationType;
|
||||||
SupportsDescending = supportsDescending;
|
|
||||||
TimePeriodFilterSupport = timePeriodSupport;
|
TimePeriodFilterSupport = timePeriodSupport;
|
||||||
MaxLimit = maxLimit;
|
MaxLimit = maxLimit;
|
||||||
}
|
}
|
||||||
@ -58,11 +42,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Ascending retrieval supported: {SupportsAscending}");
|
sb.AppendLine($"Pagination type: {PaginationSupport}");
|
||||||
sb.AppendLine($"Descending retrieval supported: {SupportsDescending}");
|
|
||||||
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
||||||
sb.AppendLine($"Max limit: {MaxLimit}");
|
sb.AppendLine($"Max limit: {MaxLimit}");
|
||||||
sb.AppendLine($"Max age: {MaxAge}");
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,405 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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,10 +19,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -31,14 +27,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -35,15 +31,13 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
Asset = asset;
|
Asset = asset;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -31,14 +27,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -36,15 +32,13 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
Interval = interval;
|
Interval = interval;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,10 +27,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -39,15 +35,13 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
Symbol = symbol;
|
Symbol = symbol;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -57,15 +51,13 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
TradingMode = tradeMode;
|
TradingMode = tradeMode;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,19 +10,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter by start time
|
/// Filter by start time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime StartTime { get; set; }
|
public DateTime StartTime { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter by end time
|
/// Filter by end time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? EndTime { get; set; }
|
public DateTime EndTime { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -31,14 +27,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime endTime, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -31,14 +27,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,10 +23,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
/// <summary>
|
|
||||||
/// Data direction
|
|
||||||
/// </summary>
|
|
||||||
public DataDirection? Direction { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@ -35,15 +31,13 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
/// <param name="direction">Data direction</param>
|
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
Asset = asset;
|
Asset = asset;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
Direction = direction;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,7 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
|
|||||||
using CryptoExchange.Net.Sockets.Interfaces;
|
using CryptoExchange.Net.Sockets.Interfaces;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
@ -125,6 +123,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
|
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,6 +135,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
|
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,6 +240,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
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()!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,6 +252,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,7 +264,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
#else
|
#else
|
||||||
private readonly object _listenersLock = new object();
|
private readonly object _listenersLock = new object();
|
||||||
#endif
|
#endif
|
||||||
private ReadOnlyCollection<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private SocketStatus _status;
|
private SocketStatus _status;
|
||||||
|
|
||||||
@ -311,7 +313,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_socket.OnError += HandleErrorAsync;
|
_socket.OnError += HandleErrorAsync;
|
||||||
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
_socket.GetReconnectionUrl = GetReconnectionUrlAsync;
|
||||||
|
|
||||||
_listeners = new ReadOnlyCollection<IMessageProcessor>([]);
|
_listeners = new List<IMessageProcessor>();
|
||||||
|
|
||||||
_serializer = apiClient.CreateSerializer();
|
_serializer = apiClient.CreateSerializer();
|
||||||
}
|
}
|
||||||
@ -338,17 +340,20 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||||
|
|
||||||
|
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.IsClosingConnection = true;
|
||||||
subscription.Reset();
|
subscription.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryList = _listeners.OfType<Query>().ToList();
|
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||||
foreach (var query in queryList)
|
{
|
||||||
query.Fail(new WebError("Connection interrupted"));
|
query.Fail(new WebError("Connection interrupted"));
|
||||||
|
_listeners.Remove(query);
|
||||||
RemoveMessageProcessors(queryList);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -364,14 +369,17 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
Authenticated = false;
|
Authenticated = false;
|
||||||
_lastSequenceNumber = 0;
|
_lastSequenceNumber = 0;
|
||||||
|
|
||||||
|
lock (_listenersLock)
|
||||||
|
{
|
||||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||||
subscription.Reset();
|
subscription.Reset();
|
||||||
|
|
||||||
var queryList = _listeners.OfType<Query>().ToList();
|
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||||
foreach (var query in queryList)
|
{
|
||||||
query.Fail(new WebError("Connection interrupted"));
|
query.Fail(new WebError("Connection interrupted"));
|
||||||
|
_listeners.Remove(query);
|
||||||
RemoveMessageProcessors(queryList);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionLost?.Invoke());
|
_ = Task.Run(() => ConnectionLost?.Invoke());
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -393,11 +401,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
Status = SocketStatus.Resubscribing;
|
Status = SocketStatus.Resubscribing;
|
||||||
|
|
||||||
var queryList = _listeners.OfType<Query>().ToList();
|
lock (_listenersLock)
|
||||||
foreach (var query in queryList)
|
{
|
||||||
|
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||||
|
{
|
||||||
query.Fail(new WebError("Connection interrupted"));
|
query.Fail(new WebError("Connection interrupted"));
|
||||||
|
_listeners.Remove(query);
|
||||||
RemoveMessageProcessors(queryList);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Can't wait for this as it would cause a deadlock
|
// Can't wait for this as it would cause a deadlock
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@ -452,7 +463,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
|
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
|
||||||
{
|
{
|
||||||
var query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
Query? query;
|
||||||
|
lock (_listenersLock)
|
||||||
|
{
|
||||||
|
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||||
|
}
|
||||||
|
|
||||||
if (query == null)
|
if (query == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
@ -476,7 +492,12 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <param name="requestId">Id of the request sent</param>
|
/// <param name="requestId">Id of the request sent</param>
|
||||||
protected virtual Task HandleRequestSentAsync(int requestId)
|
protected virtual Task HandleRequestSentAsync(int requestId)
|
||||||
{
|
{
|
||||||
var query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
Query? query;
|
||||||
|
lock (_listenersLock)
|
||||||
|
{
|
||||||
|
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||||
|
}
|
||||||
|
|
||||||
if (query == null)
|
if (query == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
@ -522,6 +543,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
Type? deserializationType = null;
|
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)
|
||||||
@ -536,6 +559,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (deserializationType != null)
|
if (deserializationType != null)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (deserializationType == null)
|
if (deserializationType == null)
|
||||||
{
|
{
|
||||||
@ -581,8 +605,19 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
var topicFilter = messageConverter.GetTopicFilter(result);
|
var topicFilter = messageConverter.GetTopicFilter(result);
|
||||||
|
|
||||||
bool processed = false;
|
bool processed = false;
|
||||||
foreach (var processor in _listeners)
|
lock (_listenersLock)
|
||||||
{
|
{
|
||||||
|
var currentCount = _listeners.Count;
|
||||||
|
for(var i = 0; i < _listeners.Count; i++)
|
||||||
|
{
|
||||||
|
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;
|
bool isQuery = false;
|
||||||
Query? query = null;
|
Query? query = null;
|
||||||
if (processor is Query cquery)
|
if (processor is Query cquery)
|
||||||
@ -634,10 +669,13 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (complete)
|
if (complete)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!processed)
|
if (!processed)
|
||||||
{
|
{
|
||||||
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||||
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
{
|
{
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(
|
_logger.ReceivedMessageNotMatchedToAnyListener(
|
||||||
SocketId,
|
SocketId,
|
||||||
@ -647,6 +685,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connect the websocket
|
/// Connect the websocket
|
||||||
@ -688,11 +727,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||||
|
|
||||||
|
lock (_listenersLock)
|
||||||
|
{
|
||||||
foreach (var subscription in _listeners.OfType<Subscription>())
|
foreach (var subscription in _listeners.OfType<Subscription>())
|
||||||
{
|
{
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _socket.CloseAsync().ConfigureAwait(false);
|
await _socket.CloseAsync().ConfigureAwait(false);
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
@ -721,12 +763,20 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
|
|
||||||
bool anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
bool anyDuplicateSubscription;
|
||||||
bool shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
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;
|
||||||
|
|
||||||
if (!anyDuplicateSubscription)
|
if (!anyDuplicateSubscription)
|
||||||
{
|
{
|
||||||
var needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
bool needUnsub;
|
||||||
|
lock (_listenersLock)
|
||||||
|
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
||||||
|
|
||||||
if (needUnsub && _socket.IsOpen)
|
if (needUnsub && _socket.IsOpen)
|
||||||
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@ -750,7 +800,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveMessageProcessor(subscription);
|
lock (_listenersLock)
|
||||||
|
_listeners.Remove(subscription);
|
||||||
|
|
||||||
subscription.Status = SubscriptionStatus.Closed;
|
subscription.Status = SubscriptionStatus.Closed;
|
||||||
}
|
}
|
||||||
@ -774,7 +825,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
AddMessageProcessor(subscription);
|
lock (_listenersLock)
|
||||||
|
_listeners.Add(subscription);
|
||||||
|
|
||||||
if (subscription.UserSubscription)
|
if (subscription.UserSubscription)
|
||||||
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
||||||
@ -787,6 +839,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
public Subscription? GetSubscription(int id)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -835,12 +888,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default)
|
private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
AddMessageProcessor(query);
|
lock (_listenersLock)
|
||||||
|
_listeners.Add(query);
|
||||||
|
|
||||||
var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false);
|
var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false);
|
||||||
if (!sendResult)
|
if (!sendResult)
|
||||||
{
|
{
|
||||||
query.Fail(sendResult.Error!);
|
query.Fail(sendResult.Error!);
|
||||||
RemoveMessageProcessor(query);
|
lock (_listenersLock)
|
||||||
|
_listeners.Remove(query);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -871,7 +927,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
RemoveMessageProcessor(query);
|
lock (_listenersLock)
|
||||||
|
_listeners.Remove(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -977,7 +1034,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
var anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
bool anySubscriptions;
|
||||||
|
lock (_listenersLock)
|
||||||
|
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||||
if (!anySubscriptions)
|
if (!anySubscriptions)
|
||||||
{
|
{
|
||||||
// No need to resubscribe anything
|
// No need to resubscribe anything
|
||||||
@ -987,8 +1046,13 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
bool anyAuthenticated;
|
||||||
|
lock (_listenersLock)
|
||||||
|
{
|
||||||
|
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||||
|| DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated;
|
|| DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
if (anyAuthenticated)
|
if (anyAuthenticated)
|
||||||
{
|
{
|
||||||
// If we reconnected a authenticated connection we need to re-authenticate
|
// If we reconnected a authenticated connection we need to re-authenticate
|
||||||
@ -1011,7 +1075,10 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
var subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
List<Subscription> subList;
|
||||||
|
lock (_listenersLock)
|
||||||
|
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||||
|
|
||||||
if (subList.Count == 0)
|
if (subList.Count == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -1193,37 +1260,6 @@ 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,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
if (message.Contains("Cannot map"))
|
if (message.Contains("Cannot map"))
|
||||||
throw new Exception("Enum value error: " + message);
|
throw new Exception("Enum value error: " + message);
|
||||||
|
|
||||||
if (message.Contains("Received null or empty enum value"))
|
if (message.Contains("Received null enum value"))
|
||||||
throw new Exception("Enum null error: " + message);
|
throw new Exception("Enum null error: " + message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
if (message.Contains("Cannot map"))
|
if (message.Contains("Cannot map"))
|
||||||
throw new Exception("Enum value error: " + message);
|
throw new Exception("Enum value error: " + message);
|
||||||
|
|
||||||
if (message.Contains("Received null or empty enum value"))
|
if (message.Contains("Received null enum value"))
|
||||||
throw new Exception("Enum null error: " + message);
|
throw new Exception("Enum null error: " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
Content = Encoding.UTF8.GetString(data);
|
Content = Encoding.UTF8.GetString(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetContent(string data, Encoding? encoding, string contentType)
|
public void SetContent(string data, string contentType)
|
||||||
{
|
{
|
||||||
Content = data;
|
Content = data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,8 +112,6 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
|
|
||||||
public async Task ReconnectAsync()
|
public async Task ReconnectAsync()
|
||||||
{
|
{
|
||||||
await Task.Delay(1).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (OnReconnecting != null)
|
if (OnReconnecting != null)
|
||||||
await OnReconnecting().ConfigureAwait(false);
|
await OnReconnecting().ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Testing.Comparers;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -61,17 +59,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
/// <typeparam name="T">Type of response</typeparam>
|
/// <typeparam name="T">Type of response</typeparam>
|
||||||
/// <param name="expression">The call expression</param>
|
/// <param name="expression">The call expression</param>
|
||||||
/// <param name="authRequest">Whether this is an authenticated request</param>
|
/// <param name="authRequest">Whether this is an authenticated request</param>
|
||||||
/// <param name="checkMissingFields">Whether to check the response for missing fields</param>
|
public async Task RunAndCheckResult<T>(Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest)
|
||||||
/// <param name="compareNestedProperty">Nested property to use for comparing when checking for missing fields</param>
|
|
||||||
/// <param name="ignoreProperties">Properties to ignore when checking for missing fields</param>
|
|
||||||
/// <param name="useSingleArrayItem">Whether to use the single array item as compare when checking for missing fields</param>
|
|
||||||
public async Task RunAndCheckResult<T>(
|
|
||||||
Expression<Func<TClient, Task<WebCallResult<T>>>> expression,
|
|
||||||
bool authRequest,
|
|
||||||
bool checkMissingFields = false,
|
|
||||||
string? compareNestedProperty = null,
|
|
||||||
List<string>? ignoreProperties = null,
|
|
||||||
bool? useSingleArrayItem = null)
|
|
||||||
{
|
{
|
||||||
if (!ShouldRun())
|
if (!ShouldRun())
|
||||||
return;
|
return;
|
||||||
@ -105,22 +93,6 @@ namespace CryptoExchange.Net.Testing
|
|||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
||||||
|
|
||||||
if (checkMissingFields)
|
|
||||||
{
|
|
||||||
var data = result.Data;
|
|
||||||
var originalData = result.OriginalData;
|
|
||||||
if (originalData == null)
|
|
||||||
throw new Exception($"Original data needs to be enabled in the client options to check for missing fields");
|
|
||||||
|
|
||||||
try {
|
|
||||||
SystemTextJsonComparer.CompareData(expressionBody.Method.Name, data, originalData, compareNestedProperty, ignoreProperties, useSingleArrayItem ?? false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception($"Compare failed: {ex.Message}; original data: {originalData}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using CryptoExchange.Net.Testing.Comparers;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Testing
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Validator for REST requests, comparing path, http method, authentication and response parsing
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TClient">The Rest client</typeparam>
|
|
||||||
public class SharedRestRequestValidator<TClient> where TClient : BaseRestClient
|
|
||||||
{
|
|
||||||
private readonly TClient _client;
|
|
||||||
private readonly Func<WebCallResult, bool> _isAuthenticated;
|
|
||||||
private readonly string _folder;
|
|
||||||
private readonly string _baseAddress;
|
|
||||||
private readonly string? _nestedPropertyForCompare;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">Client to test</param>
|
|
||||||
/// <param name="folder">Folder for json test values</param>
|
|
||||||
/// <param name="baseAddress">The base address that is expected</param>
|
|
||||||
/// <param name="isAuthenticated">Func for checking if the request is authenticated</param>
|
|
||||||
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
|
||||||
public SharedRestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null)
|
|
||||||
{
|
|
||||||
_client = client;
|
|
||||||
_folder = folder;
|
|
||||||
_baseAddress = baseAddress;
|
|
||||||
_nestedPropertyForCompare = nestedPropertyForCompare;
|
|
||||||
_isAuthenticated = isAuthenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate a request
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
|
||||||
/// <param name="methodInvoke">Method invocation</param>
|
|
||||||
/// <param name="name">Method name for looking up json test values</param>
|
|
||||||
/// <param name="endpointOptions">Request options</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="Exception"></exception>
|
|
||||||
public Task ValidateAsync<TResponse>(
|
|
||||||
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
|
|
||||||
string name,
|
|
||||||
EndpointOptions endpointOptions,
|
|
||||||
params Func<TResponse, bool>[] validation)
|
|
||||||
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, endpointOptions, validation);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate a request
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TResponse">Expected response type</typeparam>
|
|
||||||
/// <typeparam name="TActualResponse">The concrete response type</typeparam>
|
|
||||||
/// <param name="methodInvoke">Method invocation</param>
|
|
||||||
/// <param name="name">Method name for looking up json test values</param>
|
|
||||||
/// <param name="endpointOptions">Request options</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="Exception"></exception>
|
|
||||||
public async Task ValidateAsync<TResponse, TActualResponse>(
|
|
||||||
Func<TClient, Task<ExchangeWebResult<TResponse>>> methodInvoke,
|
|
||||||
string name,
|
|
||||||
EndpointOptions endpointOptions,
|
|
||||||
params Func<TResponse, bool>[] validation) where TActualResponse : TResponse
|
|
||||||
{
|
|
||||||
var listener = new EnumValueTraceListener();
|
|
||||||
Trace.Listeners.Add(listener);
|
|
||||||
|
|
||||||
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
|
||||||
FileStream file;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
throw new Exception($"Response file not found for {name}: {path}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = new byte[file.Length];
|
|
||||||
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
|
||||||
file.Close();
|
|
||||||
|
|
||||||
var data = Encoding.UTF8.GetString(buffer);
|
|
||||||
using var reader = new StringReader(data);
|
|
||||||
var expectedMethod = reader.ReadLine();
|
|
||||||
var expectedPath = reader.ReadLine();
|
|
||||||
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
|
||||||
var response = reader.ReadToEnd();
|
|
||||||
|
|
||||||
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
|
||||||
var result = await methodInvoke(_client).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Check request/response properties
|
|
||||||
if (result.Error != null)
|
|
||||||
throw new Exception(name + " returned error " + result.Error);
|
|
||||||
if (endpointOptions.NeedsAuthentication != expectedAuth)
|
|
||||||
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
|
|
||||||
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
|
||||||
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
|
||||||
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
|
||||||
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
|
||||||
|
|
||||||
var index = 0;
|
|
||||||
foreach(var validate in validation)
|
|
||||||
{
|
|
||||||
if (!validate(result.Data!))
|
|
||||||
throw new Exception(name + $" response validation #{index} failed");
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Listeners.Remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -332,8 +332,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
_data.Add(item.OpenTime, item);
|
_data.Add(item.OpenTime, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
_firstTimestamp = _data.Count == 0 ? null : _data.Min(v => v.Key);
|
_firstTimestamp = _data.Min(v => v.Key);
|
||||||
|
|
||||||
ApplyWindow(false);
|
ApplyWindow(false);
|
||||||
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
||||||
}
|
}
|
||||||
@ -376,7 +375,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_firstTimestamp = _data.Count == 0 ? null : _data.Min(x => x.Key);
|
_firstTimestamp = _data.Min(x => x.Key);
|
||||||
_changed = true;
|
_changed = true;
|
||||||
|
|
||||||
SetSyncStatus();
|
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