mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
150 Commits
5.2.1
...
7.0.0-beta2
| Author | SHA1 | Date | |
|---|---|---|---|
| 30e100d01a | |||
| 4f79c0ed10 | |||
| f917bf0e3f | |||
| 68e525ab9d | |||
| 571dc26d5d | |||
| 63249a99db | |||
| e1c5c3d19f | |||
| f33857768b | |||
| 46da3b40a0 | |||
| bbad338941 | |||
| 3e290b185e | |||
| bdbec48155 | |||
| 76e95cf870 | |||
| da1686d95c | |||
| 22cd2268ab | |||
| 8d45b4b069 | |||
| ae4a8bdc32 | |||
| 764a56ec7f | |||
| ad28d1e00e | |||
| 1c5e9b0d64 | |||
| 7ac75b9abe | |||
| d2dc8a06e5 | |||
| 9ddd446892 | |||
| 1eb0214e7a | |||
| 4e4ffcab1c | |||
| 0d6a100aac | |||
| 99df7cc792 | |||
| 3fa8277a30 | |||
| d0fc67355d | |||
| 8a869e8e1d | |||
| acd9b0d533 | |||
| fc6503035a | |||
| e3207033c3 | |||
| b057974cd0 | |||
| 9ead87d350 | |||
| c1ee36dd8a | |||
| eee19b28a5 | |||
| 58098edaa6 | |||
| c931a60cb7 | |||
| 12d5783625 | |||
| b640690a0f | |||
| c41e128900 | |||
| 1ba66be29f | |||
| ff6a9d5f13 | |||
| b59fe9e3ef | |||
| ac434fa2c6 | |||
| 3de04e4828 | |||
| 081c2d4268 | |||
| 6fa66d819d | |||
| 312d54cf04 | |||
| 35f7dbf9fb | |||
| 5539320827 | |||
| cf941fe5c9 | |||
| ad3959a8e9 | |||
| 9f92d86855 | |||
| bee2e86c2f | |||
| bf854c92af | |||
| 141d5bd956 | |||
| cff3863373 | |||
| d91755dff5 | |||
| 27d49a6093 | |||
| ad1bdd9a3f | |||
| 4f2d7abc7e | |||
| ef9de5e338 | |||
| 8b513e51b9 | |||
| d43b38a23a | |||
| 0987c0f9d1 | |||
| e2dde77023 | |||
| 104ac7caad | |||
| 8788dd3deb | |||
| f64cc5e9cf | |||
| 75d1bbc6e8 | |||
| b621aa7e65 | |||
| 9783108695 | |||
| 6ba32fe280 | |||
| f75cc75bbc | |||
| 2109b65a8e | |||
| a472751638 | |||
| f08ed16f2a | |||
| 212d457a6a | |||
| ac5f333766 | |||
| 640e4387c1 | |||
| a16b19019f | |||
| 2443f576ac | |||
| 4fd7e44015 | |||
| a0a3bda1c5 | |||
| 6bda7a3c73 | |||
| 69a7a714cd | |||
| 48e2e6468e | |||
| 4017ac780f | |||
| a55cd1bb13 | |||
| b34129e148 | |||
| be25a68c9c | |||
| 468cd5e48e | |||
| 262c4e4aa5 | |||
| 4ccff6461f | |||
| 3bfa3ef389 | |||
| 5238971bcc | |||
| 2f5c904faf | |||
| c62775813f | |||
| f11b3754f0 | |||
| 3cbe0465e9 | |||
| 5048aea722 | |||
| 8d35339ab2 | |||
| c3316a51e7 | |||
| 7ecf37064b | |||
| 18954f4f53 | |||
| 00bc245102 | |||
| 273cab9fdb | |||
| 84d0f0ec9e | |||
| 690f2a63e5 | |||
| 19cc020852 | |||
| 1d4353e6d1 | |||
| a02b3f88d7 | |||
| af44ca4c9f | |||
| cf2b57bb96 | |||
| d0a2288910 | |||
| 89c11afc21 | |||
| da6ed580f1 | |||
| 1c33e297e7 | |||
| 0005534a95 | |||
| 11650f7c1a | |||
| a222bb3f02 | |||
| 6361c5ef25 | |||
| 892e8a4508 | |||
| 8336d373f3 | |||
| 71072680a8 | |||
| e13f105019 | |||
| 401577451e | |||
| 5c41ef1ee4 | |||
| ad614830d1 | |||
| 3365837338 | |||
| 66ac2972d6 | |||
| 0d3e05880a | |||
| 997e71f3b7 | |||
| b0fca4587d | |||
| c10671768d | |||
| 91e8123679 | |||
| 417cf2f9ac | |||
| 277be7ab9b | |||
| 45f3459f59 | |||
| 98dad4a8ed | |||
| 1e5f19271b | |||
| 8abeeb4cf0 | |||
| cae0cd9ead | |||
| 811574ae01 | |||
| 0ddecf7f8d | |||
| 5bcf50fb4d | |||
| 9f0654815d | |||
| 465e9f04f4 |
@@ -287,5 +287,3 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||
/Docs/*
|
||||
Docs/
|
||||
|
||||
@@ -11,66 +11,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture()]
|
||||
public class BaseClientTests
|
||||
{
|
||||
[TestCase]
|
||||
public void SettingLogOutput_Should_RedirectLogOutput()
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new BaseRestClientOptions()
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
// act
|
||||
client.Log(LogLevel.Information, "Test");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
|
||||
}
|
||||
|
||||
[TestCase(LogLevel.None, LogLevel.Error, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
|
||||
[TestCase(null, LogLevel.Error, true)]
|
||||
[TestCase(null, LogLevel.Warning, true)]
|
||||
[TestCase(null, LogLevel.Information, true)]
|
||||
[TestCase(null, LogLevel.Debug, false)]
|
||||
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var options = new BaseRestClientOptions()
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
};
|
||||
if (verbosity != null)
|
||||
options.LogLevel = verbosity.Value;
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// act
|
||||
client.Log(testVerbosity, "Test");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
{
|
||||
@@ -78,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123}");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -91,7 +31,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
|
||||
@@ -113,7 +113,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
@@ -140,7 +142,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
@@ -162,6 +164,44 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", null)]
|
||||
public void TestBoolConverter(string value, bool? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", false)]
|
||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
}
|
||||
|
||||
public class TimeObject
|
||||
@@ -180,6 +220,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
public class BoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableBoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public enum TestEnum
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
@@ -34,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException),
|
||||
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -57,239 +58,96 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestApiOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
BaseAddress = "http://test1.com"
|
||||
},
|
||||
Api2Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("789", "101"),
|
||||
BaseAddress = "http://test2.com"
|
||||
}
|
||||
};
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||
Assert.AreEqual(options.Api2Options.BaseAddress, "http://test2.com");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotOverridenApiOptionsAreStillDefault()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.RateLimitingBehaviour, RateLimitingBehaviour.Wait);
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||
Assert.AreEqual(options.Api2Options.BaseAddress, "https://api2.test.com/");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultBaseOptionsAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.LogLevel, LogLevel.Trace);
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultApiOptionsAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("456", "789")
|
||||
}
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "789");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultApiOptionsWithSomeOverriddenAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("456", "789")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "222");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
var client = new TestRestClient(options => {
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient();
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient(new TestClientOptions
|
||||
var client = new TestRestClient(options =>
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions()
|
||||
{
|
||||
BaseAddress = "http://test.com"
|
||||
}
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "444");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClientOptions: BaseRestClientOptions
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </summary>
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions();
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||
{
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
|
||||
public RestApiClientOptions Api1Options
|
||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
{
|
||||
get => _api1Options;
|
||||
set => _api1Options = new RestApiClientOptions(_api1Options, value);
|
||||
}
|
||||
|
||||
private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/");
|
||||
public RestApiClientOptions Api2Options
|
||||
{
|
||||
get => _api2Options;
|
||||
set => _api2Options = new RestApiClientOptions(_api2Options, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions(): this(Default)
|
||||
{
|
||||
}
|
||||
|
||||
public TestClientOptions(TestClientOptions baseOn): base(baseOn)
|
||||
{
|
||||
if (baseOn == null)
|
||||
return;
|
||||
|
||||
ReceiveWindow = baseOn.ReceiveWindow;
|
||||
|
||||
Api1Options = new RestApiClientOptions(baseOn.Api1Options, null);
|
||||
Api2Options = new RestApiClientOptions(baseOn.Api2Options, null);
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -28,7 +27,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -43,7 +42,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetResponse("{\"property\": 123", out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -58,7 +57,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||
|
||||
// act
|
||||
var result = await client.Request<TestObject>();
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -73,7 +72,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = await client.Request<TestObject>();
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -91,7 +90,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = await client.Request<TestObject>();
|
||||
var result = await client.Api2.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -106,23 +105,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||
},
|
||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||
});
|
||||
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
|
||||
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
|
||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@@ -136,19 +128,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
}
|
||||
});
|
||||
var client = new TestRestClient();
|
||||
|
||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
||||
|
||||
client.SetResponse("{}", out var request);
|
||||
|
||||
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
{
|
||||
{ "TestParam1", "Value1" },
|
||||
{ "TestParam2", 2 },
|
||||
@@ -175,20 +161,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
@@ -199,15 +182,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
@@ -218,14 +198,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
@@ -236,20 +213,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
@@ -258,15 +232,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
@@ -278,15 +249,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
@@ -315,14 +283,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
@@ -332,14 +297,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
@@ -350,15 +312,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test", true, true, false, true)]
|
||||
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Constraints;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -17,19 +23,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
SubOptions = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
},
|
||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
options.SubOptions.MaxSocketConnections = 1;
|
||||
});
|
||||
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6);
|
||||
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@@ -42,61 +45,70 @@ namespace CryptoExchange.Net.UnitTests
|
||||
socket.CanConnect = canConnect;
|
||||
|
||||
//act
|
||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket, null));
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, null, socket, null);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
Dictionary<string, string> result = null;
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.JsonData;
|
||||
result = messageEvent.Data;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
await socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue((int)result["property"] == 123);
|
||||
Assert.IsTrue(result["property"] == "123");
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
options.SubOptions.OutputOriginalData = enabled;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, null, socket, null);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
await socket.InvokeMessage("{\"property\": 123}");
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
@@ -107,14 +119,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(client, null, socket, null);
|
||||
client.ConnectSocketSub(sub);
|
||||
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||
var ups = new UpdateSubscription(sub, us);
|
||||
sub.AddSubscription(us);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var ups = new UpdateSubscription(sub, subscription);
|
||||
sub.AddSubscription(subscription);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAsync(ups).Wait();
|
||||
@@ -127,15 +143,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket1 = client.CreateSocket();
|
||||
var socket2 = client.CreateSocket();
|
||||
socket1.CanConnect = true;
|
||||
socket2.CanConnect = true;
|
||||
var sub1 = new SocketConnection(client, null, socket1, null);
|
||||
var sub2 = new SocketConnection(client, null, socket2, null);
|
||||
client.ConnectSocketSub(sub1);
|
||||
client.ConnectSocketSub(sub2);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||
client.SubClient.ConnectSocketSub(sub1);
|
||||
client.SubClient.ConnectSocketSub(sub2);
|
||||
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
|
||||
sub1.AddSubscription(subscription1);
|
||||
sub2.AddSubscription(subscription2);
|
||||
var ups1 = new UpdateSubscription(sub1, subscription1);
|
||||
var ups2 = new UpdateSubscription(sub2, subscription2);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAllAsync().Wait();
|
||||
@@ -149,13 +172,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub = new SocketConnection(client, null, socket, null);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
|
||||
// act
|
||||
var connectResult = client.ConnectSocketSub(sub);
|
||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(connectResult.Success);
|
||||
|
||||
@@ -4,8 +4,9 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.OrderBook;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -13,12 +14,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture]
|
||||
public class SymbolOrderBookTests
|
||||
{
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
|
||||
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base("Test", "BTC/USD", defaultOrderBookOptions)
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(_defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +37,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
||||
{
|
||||
Status = OrderBookStatus.Synced;
|
||||
base.bids.Clear();
|
||||
base._bids.Clear();
|
||||
foreach (var bid in bids)
|
||||
base.bids.Add(bid.Price, bid);
|
||||
base.asks.Clear();
|
||||
base._bids.Add(bid.Price, bid);
|
||||
base._asks.Clear();
|
||||
foreach (var ask in asks)
|
||||
base.asks.Add(ask.Price, ask);
|
||||
base._asks.Add(ask.Price, ask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,5 +110,33 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23333333m, resultAsks2.Data);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void CalculateTradableAmount()
|
||||
{
|
||||
var orderbook = new TestableSymbolOrderBook();
|
||||
orderbook.SetData(new List<ISymbolOrderBookEntry>
|
||||
{
|
||||
new BookEntry{ Price = 1, Quantity = 1 },
|
||||
new BookEntry{ Price = 1.1m, Quantity = 1 },
|
||||
},
|
||||
new List<ISymbolOrderBookEntry>()
|
||||
{
|
||||
new BookEntry{ Price = 1.2m, Quantity = 1 },
|
||||
new BookEntry{ Price = 1.3m, Quantity = 1 },
|
||||
});
|
||||
|
||||
var resultBids = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Bid);
|
||||
var resultAsks = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Ask);
|
||||
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.9m, resultBids.Data);
|
||||
Assert.AreEqual(1.61538462m, resultAsks.Data);
|
||||
Assert.AreEqual(1.4m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23076923m, resultAsks2.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class TestQuery : Query<object>
|
||||
{
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
||||
|
||||
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
ListenerIdentifiers = new HashSet<string> { identifier };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class TestSubscription<T> : Subscription<object, object>
|
||||
{
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
|
||||
|
||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
var data = (T)message.Data;
|
||||
_handler.Invoke(message.As(data));
|
||||
return Task.FromResult(new CallResult(null));
|
||||
}
|
||||
|
||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
||||
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public class TestBaseClient: BaseClient
|
||||
{
|
||||
public TestBaseClient(): base("Test", new BaseClientOptions())
|
||||
public TestSubClient SubClient { get; }
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public void Log(LogLevel verbosity, string data)
|
||||
{
|
||||
log.Write(verbosity, data);
|
||||
_logger.Log(verbosity, data);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubClient : RestApiClient
|
||||
{
|
||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
return Deserialize<T>(data, null, null);
|
||||
}
|
||||
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
|
||||
|
||||
public override TimeSpan? GetTimeOffset() => null;
|
||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
@@ -41,9 +60,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public override string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -20,15 +22,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient() : this(new TestClientOptions())
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
{
|
||||
Api1 = new TestRestApi1Client(exchangeOptions);
|
||||
Api2 = new TestRestApi2Client(exchangeOptions);
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@@ -50,7 +59,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
{
|
||||
@@ -58,6 +67,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
requestObj = request.Object;
|
||||
}
|
||||
|
||||
@@ -71,7 +89,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
@@ -94,27 +117,33 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||
{
|
||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options)
|
||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
}
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
@@ -122,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public override TimeSpan GetTimeOffset()
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -143,12 +172,24 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options)
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||
{
|
||||
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public override TimeSpan GetTimeOffset()
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||
{
|
||||
var errorData = ValidateJson(data);
|
||||
|
||||
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -167,28 +208,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
}
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
{
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
||||
{
|
||||
uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||
bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
|
||||
protected override Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -12,12 +14,16 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public bool CanConnect { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
|
||||
public event Action OnClose;
|
||||
public event Action OnReconnected;
|
||||
public event Action OnReconnecting;
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
public event Func<Task> OnClose;
|
||||
#pragma warning disable 0067
|
||||
public event Func<Task> OnReconnected;
|
||||
public event Func<Task> OnReconnecting;
|
||||
#pragma warning restore 0067
|
||||
public event Func<int, Task> OnRequestSent;
|
||||
public event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
|
||||
public event Func<Exception, Task> OnError;
|
||||
public event Func<Task> OnOpen;
|
||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
|
||||
public int Id { get; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
@@ -65,10 +71,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
return Task.FromResult(CanConnect);
|
||||
}
|
||||
|
||||
public void Send(string data)
|
||||
public void Send(int requestId, string data, int weight)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
@@ -104,9 +111,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
|
||||
public void InvokeMessage(string data)
|
||||
public async Task InvokeMessage(string data)
|
||||
{
|
||||
OnMessage?.Invoke(data);
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
||||
await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream);
|
||||
}
|
||||
|
||||
public void SetProxy(ApiProxy proxy)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@@ -14,74 +18,78 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient() : this(new TestOptions())
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
return (TestSocket)CreateSocket("https://localhost:123/");
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
||||
{
|
||||
public static TestSocketOptions Default = new TestSocketOptions
|
||||
{
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal IWebsocket CreateSocketInternal(string address)
|
||||
{
|
||||
return CreateSocket(address);
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub).Result;
|
||||
}
|
||||
|
||||
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
|
||||
out CallResult<object> callResult)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestOptions: BaseSocketClientOptions
|
||||
{
|
||||
public ApiClientOptions SubOptions { get; set; } = new ApiClientOptions();
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
|
||||
public TestSubSocketClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
public override string GetListenerIdentifier(IMessageAccessor messageAccessor) => "topic";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestStringLogger : ILogger
|
||||
{
|
||||
StringBuilder _builder = new StringBuilder();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
_builder.AppendLine(formatter(state, exception));
|
||||
}
|
||||
|
||||
public string GetLogs()
|
||||
{
|
||||
return _builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
||||
@@ -22,17 +22,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
public SecureString? Secret { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key to authenticate requests
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public PrivateKey? PrivateKey { get; }
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing a private key for authentication
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key used for signing</param>
|
||||
public ApiCredentials(PrivateKey privateKey)
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,11 +40,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret)
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
@@ -54,11 +56,22 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(string key, string secret)
|
||||
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
}
|
||||
@@ -69,11 +82,8 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
if (PrivateKey == null)
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||
else
|
||||
return new ApiCredentials(PrivateKey!.Copy());
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,7 +133,6 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
Key?.Dispose();
|
||||
Secret?.Dispose();
|
||||
PrivateKey?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,15 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Base class for authentication providers
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The provided credentials
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
public ApiCredentials Credentials { get; }
|
||||
protected readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
@@ -32,7 +33,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
if (credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
Credentials = credentials;
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
}
|
||||
|
||||
@@ -73,6 +74,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA256Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -83,7 +95,20 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes): BytesToHexString(resultBytes);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -99,6 +124,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -112,6 +172,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -126,28 +221,70 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignMD5Bytes(string data)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <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 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)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <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 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(string data, SignOutputType? outputType = null)
|
||||
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
@@ -174,23 +311,80 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// SHA256 sign the data
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string Sign(string toSign)
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
return toSign;
|
||||
using var rsa = CreateRSA();
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a byte array
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
public virtual byte[] Sign(byte[] toSign)
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
return toSign;
|
||||
using var rsa = CreateRSA();
|
||||
using var sha384 = SHA384.Create();
|
||||
var hash = sha384.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha512 = SHA512.Create();
|
||||
var hash = sha512.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!.GetString()
|
||||
.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!.GetString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
return rsa;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -223,7 +417,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -235,5 +429,26 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key info
|
||||
/// </summary>
|
||||
public class PrivateKey : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The private key
|
||||
/// </summary>
|
||||
public SecureString Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key's pass phrase
|
||||
/// </summary>
|
||||
public SecureString? Passphrase { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the private key is encrypted or not
|
||||
/// </summary>
|
||||
public bool IsEncrypted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(SecureString key, SecureString passphrase)
|
||||
{
|
||||
Key = key;
|
||||
Passphrase = passphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(string key, string passphrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
|
||||
throw new ArgumentException("Key and passphrase can't be null/empty");
|
||||
|
||||
var secureKey = new SecureString();
|
||||
foreach (var c in key)
|
||||
secureKey.AppendChar(c);
|
||||
secureKey.MakeReadOnly();
|
||||
Key = secureKey;
|
||||
|
||||
var securePassphrase = new SecureString();
|
||||
foreach (var c in passphrase)
|
||||
securePassphrase.AppendChar(c);
|
||||
securePassphrase.MakeReadOnly();
|
||||
Passphrase = securePassphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an unencrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(SecureString key)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentException("Key can't be null/empty");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the private key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PrivateKey Copy()
|
||||
{
|
||||
if (Passphrase == null)
|
||||
return new PrivateKey(Key.GetString());
|
||||
else
|
||||
return new PrivateKey(Key.GetString(), Passphrase.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Passphrase?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient: IDisposable
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
private ApiCredentials? _apiCredentials;
|
||||
private AuthenticationProvider? _authenticationProvider;
|
||||
private bool _created;
|
||||
private bool _disposing;
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// If we are disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_created && !_disposing && _apiCredentials != null)
|
||||
{
|
||||
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
|
||||
_created = true;
|
||||
}
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
}
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
@@ -65,25 +67,53 @@ namespace CryptoExchange.Net
|
||||
public string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// The base address for this API client
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
internal protected string BaseAddress { get; }
|
||||
public string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api client options
|
||||
/// Output the original string data along with the deserialized object
|
||||
/// </summary>
|
||||
internal ApiClientOptions Options { get; }
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default serializer
|
||||
/// </summary>
|
||||
protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
public ApiOptions ApiOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Options
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="apiOptions">Api client options</param>
|
||||
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions)
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
/// <param name="apiOptions">Api options</param>
|
||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||
{
|
||||
Options = apiOptions;
|
||||
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
|
||||
BaseAddress = apiOptions.BaseAddress;
|
||||
_logger = logger;
|
||||
|
||||
ClientOptions = clientOptions;
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -94,21 +124,211 @@ namespace CryptoExchange.Net
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials(ApiCredentials credentials)
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
_apiCredentials = credentials?.Copy();
|
||||
_created = false;
|
||||
_authenticationProvider = null;
|
||||
if (credentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data));
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= DefaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= DefaultSerializer;
|
||||
string? data = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (OutputOriginalData == true)
|
||||
{
|
||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
var result = Deserialize<T>(data, serializer, requestId);
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
_apiCredentials?.Dispose();
|
||||
AuthenticationProvider?.Credentials?.Dispose();
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -22,54 +16,58 @@ namespace CryptoExchange.Net
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
internal string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal Log log;
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
protected static int lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
protected static object idLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
protected internal ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public BaseClientOptions ClientOptions { get; }
|
||||
public ExchangeOptions ClientOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseClient(string name, BaseClientOptions options)
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string name)
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
log = new Log(name);
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
options.OnLoggingChanged += HandleLogConfigChange;
|
||||
|
||||
ClientOptions = options;
|
||||
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
Name = name;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
/// <summary>
|
||||
/// Initialize the client with the specified options
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
protected virtual void Initialize(ExchangeOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -78,227 +76,20 @@ namespace CryptoExchange.Net
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||
{
|
||||
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
|
||||
if (ClientOptions == null)
|
||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||
|
||||
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
|
||||
ApiClients.Add(apiClient);
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data));
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>( tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= defaultSerializer;
|
||||
string? data = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (ClientOptions.OutputOriginalData || log.Level == LogLevel.Trace)
|
||||
{
|
||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||
var result = Deserialize<T>(data, serializer, requestId);
|
||||
if(ClientOptions.OutputOriginalData)
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected static int NextId()
|
||||
{
|
||||
lock (idLock)
|
||||
{
|
||||
lastId += 1;
|
||||
return lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a change in the client options log config
|
||||
/// </summary>
|
||||
private void HandleLogConfigChange()
|
||||
{
|
||||
log.UpdateWriters(ClientOptions.LogWriters);
|
||||
log.Level = ClientOptions.LogLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Disposing client");
|
||||
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
|
||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -22,475 +9,16 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating requests. Used for unit testing
|
||||
/// </summary>
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||
|
||||
/// <summary>
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
public new BaseRestClientOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials(ApiCredentials credentials)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a request to the uri and returns if it was successful
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The API client the request is for</param>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult> SendRequestAsync(RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<object>(apiClient, request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||
return result.AsDataless();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="apiClient">The API client the request is for</param>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false
|
||||
) where T : class
|
||||
{
|
||||
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult<T>(request.Error!);
|
||||
|
||||
return await GetResponseAsync<T>(apiClient, request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a request to be sent to the server
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The API client the request is for</param>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
var requestId = NextId();
|
||||
|
||||
if (signed)
|
||||
{
|
||||
var syncTask = apiClient.SyncTimeAsync();
|
||||
var timeSyncInfo = apiClient.GetTimeSyncInfo();
|
||||
if (timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||
{
|
||||
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||
if (!syncTimeResult)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
return syncTimeResult.As<IRequest>(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreRatelimit)
|
||||
{
|
||||
foreach (var limiter in apiClient.RateLimiters)
|
||||
{
|
||||
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult.Success)
|
||||
return new CallResult<IRequest>(limitResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (signed && apiClient.AuthenticationProvider == null)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
||||
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
|
||||
|
||||
string? paramString = "";
|
||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
|
||||
var headers = request.GetHeaders();
|
||||
if (headers.Any())
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
|
||||
apiClient.TotalRequestsMade++;
|
||||
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||
return new CallResult<IRequest>(request);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request and returns the result deserialized into the type parameter class
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client making the request</param>
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||
BaseApiClient apiClient,
|
||||
IRequest request,
|
||||
JsonSerializer? deserializer,
|
||||
CancellationToken cancellationToken,
|
||||
bool expectedEmptyResponse)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
var statusCode = response.StatusCode;
|
||||
var headers = response.ResponseHeaders;
|
||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
||||
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
||||
if (apiClient.manualParseError)
|
||||
{
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}");
|
||||
|
||||
if (!expectedEmptyResponse)
|
||||
{
|
||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
// Let the library implementation see if it is an error response, and if so parse the error
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
|
||||
// Not an error, so continue deserializing
|
||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data))
|
||||
{
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
// Not empty, and not json
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
// Error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
}
|
||||
|
||||
// Empty success response; okay
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expectedEmptyResponse)
|
||||
{
|
||||
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
}
|
||||
|
||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Http status code indicates error
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
var parseResult = ValidateJson(data);
|
||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||
if(error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
|
||||
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
|
||||
/// If the response is an error this method should return the parsed error, else it should return null
|
||||
/// </summary>
|
||||
/// <param name="data">Received data</param>
|
||||
/// <returns>Null if not an error, Error otherwise</returns>
|
||||
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
||||
{
|
||||
return Task.FromResult<ServerError?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request object
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The API client the request is for</param>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
||||
/// <param name="requestId">Unique id of a request</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest ConstructRequest(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object>? parameters,
|
||||
bool signed,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
int requestId,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
|
||||
for (var i = 0; i< parameters.Count; i++)
|
||||
{
|
||||
var kvp = parameters.ElementAt(i);
|
||||
if (kvp.Value is Func<object> delegateValue)
|
||||
parameters[kvp.Key] = delegateValue();
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||
{
|
||||
foreach (var parameter in parameters)
|
||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||
if (apiClient.AuthenticationProvider != null)
|
||||
apiClient.AuthenticationProvider.AuthenticateRequest(
|
||||
apiClient,
|
||||
uri,
|
||||
method,
|
||||
parameters,
|
||||
signed,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
out uriParameters,
|
||||
out bodyParameters,
|
||||
out headers);
|
||||
|
||||
// Sanity check
|
||||
foreach(var param in parameters)
|
||||
{
|
||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||
$"should return provided parameters in either the uri or body parameters output");
|
||||
}
|
||||
|
||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(method, uri, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
foreach (var header in additionalHeaders)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (StandardRequestHeaders != null)
|
||||
{
|
||||
foreach (var header in StandardRequestHeaders)
|
||||
// Only add it if it isn't overwritten
|
||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = apiClient.requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
WriteParamBody(apiClient, request, bodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(apiClient.requestBodyEmptyContent, contentType);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the parameters of the request to the request object body
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client making the request</param>
|
||||
/// <param name="request">The request to set the parameters on</param>
|
||||
/// <param name="parameters">The parameters to set</param>
|
||||
/// <param name="contentType">The content type of the data</param>
|
||||
protected virtual void WriteParamBody(BaseApiClient apiClient, IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||
{
|
||||
if (apiClient.requestBodyFormat == RequestBodyFormat.Json)
|
||||
{
|
||||
// Write the parameters as json in the body
|
||||
var stringData = JsonConvert.SerializeObject(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (apiClient.requestBodyFormat == RequestBodyFormat.FormData)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
||||
/// </summary>
|
||||
/// <param name="error">The string the request returned</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError(error.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -21,673 +15,27 @@ namespace CryptoExchange.Net
|
||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||
{
|
||||
#region fields
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||
|
||||
/// <summary>
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
/// </summary>
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
/// <summary>
|
||||
/// Keep alive interval for websocket connection
|
||||
/// </summary>
|
||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<byte[], string>? dataInterpreterBytes;
|
||||
/// <summary>
|
||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<string, string>? dataInterpreterString;
|
||||
/// <summary>
|
||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||
/// </summary>
|
||||
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
|
||||
/// <summary>
|
||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
|
||||
/// </summary>
|
||||
protected Task? periodicTask;
|
||||
/// <summary>
|
||||
/// Wait event for the periodicTask
|
||||
/// </summary>
|
||||
protected AsyncResetEvent? periodicEvent;
|
||||
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool disposing;
|
||||
|
||||
/// <summary>
|
||||
/// If true; data which is a response to a query will also be distributed to subscriptions
|
||||
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
|
||||
/// </summary>
|
||||
protected internal bool ContinueOnQueryResponse { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
||||
/// </summary>
|
||||
protected internal bool UnhandledMessageExpected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of outgoing messages per socket per second
|
||||
/// </summary>
|
||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
||||
|
||||
protected bool _disposing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||
}
|
||||
}
|
||||
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => socketConnections.Count;
|
||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
public new BaseSocketClientOptions ClientOptions { get; }
|
||||
|
||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
|
||||
{
|
||||
ClientOptions = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials(ApiCredentials credentials)
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a delegate to be used for processing data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
/// <param name="byteHandler">Handler for byte data</param>
|
||||
/// <param name="stringHandler">Handler for string data</param>
|
||||
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
|
||||
{
|
||||
dataInterpreterBytes = byteHandler;
|
||||
dataInterpreterString = stringHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to an url and listen for data on the BaseAddress
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||
/// <param name="apiClient">The API client the subscription is for</param>
|
||||
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||
/// <param name="dataHandler">The handler of update data</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
return SubscribeAsync(apiClient, apiClient.Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to an url and listen for data
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||
/// <param name="apiClient">The API client the subscription is for</param>
|
||||
/// <param name="url">The URL to connect to</param>
|
||||
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||
/// <param name="dataHandler">The handler of update data</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
if (disposing)
|
||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
SocketSubscription? subscription;
|
||||
var released = false;
|
||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||
// This is necessary for being able to see if connections can be combined
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
|
||||
if(!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
|
||||
if (subscription == null)
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
|
||||
return new CallResult<UpdateSubscription>( new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
// Send the request and wait for answer
|
||||
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No request to be sent, so just mark the subscription as comfirmed
|
||||
subscription.Confirmed = true;
|
||||
}
|
||||
|
||||
if (ct != default)
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}, false);
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the subscribe request and waits for a response to that request
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The connection to send the request on</param>
|
||||
/// <param name="request">The request to send, will be serialized to json</param>
|
||||
/// <param name="subscription">The subscription the request is for</param>
|
||||
/// <returns></returns>
|
||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||
{
|
||||
CallResult<object>? callResult = null;
|
||||
await socketConnection.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||
|
||||
if (callResult?.Success == true)
|
||||
{
|
||||
subscription.Confirmed = true;
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
if(callResult== null)
|
||||
return new CallResult<bool>(new ServerError("No response on subscription request received"));
|
||||
|
||||
return new CallResult<bool>(callResult.Error!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Expected result type</typeparam>
|
||||
/// <param name="apiClient">The API client the query is for</param>
|
||||
/// <param name="request">The request to send, will be serialized to json</param>
|
||||
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, object request, bool authenticated)
|
||||
{
|
||||
return QueryAsync<T>(apiClient, apiClient.Options.BaseAddress, request, authenticated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a query on a socket connection and wait for the response
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expected result type</typeparam>
|
||||
/// <param name="apiClient">The API client the query is for</param>
|
||||
/// <param name="url">The url for the request</param>
|
||||
/// <param name="request">The request to send</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, string url, object request, bool authenticated)
|
||||
{
|
||||
if (disposing)
|
||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<T>(default);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Can release early when only a single sub per connection
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<T>(connectResult.Error!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
|
||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the query request and waits for the result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expected result type</typeparam>
|
||||
/// <param name="socket">The connection to send and wait on</param>
|
||||
/// <param name="request">The request to send</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
||||
{
|
||||
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||
await socket.SendAndWaitAsync(request, ClientOptions.SocketResponseTimeout, data =>
|
||||
{
|
||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||
return false;
|
||||
|
||||
dataResult = callResult;
|
||||
return true;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return dataResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
|
||||
/// </summary>
|
||||
/// <param name="socket">The connection to check</param>
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<bool>(connectResult.Error!);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
log.Write(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||
if(socket.Connected)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult<bool>(result.Error);
|
||||
}
|
||||
|
||||
socket.Authenticated = true;
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the query that was send (the request parameter).
|
||||
/// For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
|
||||
/// anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
|
||||
/// if not some other method has be implemented to match the messages).
|
||||
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of response that is expected on the query</typeparam>
|
||||
/// <param name="socketConnection">The socket connection</param>
|
||||
/// <param name="request">The request that a response is awaited for</param>
|
||||
/// <param name="data">The message received from the server</param>
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the query</returns>
|
||||
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)]out CallResult<T>? callResult);
|
||||
/// <summary>
|
||||
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter).
|
||||
/// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
|
||||
/// anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
|
||||
/// if not some other method has be implemented to match the messages).
|
||||
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection</param>
|
||||
/// <param name="subscription">A subscription that waiting for a subscription response</param>
|
||||
/// <param name="request">The request that the subscription sent</param>
|
||||
/// <param name="data">The message received from the server</param>
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the subscription request</returns>
|
||||
protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
|
||||
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
||||
/// <param name="message">The received data</param>
|
||||
/// <param name="request">The subscription request</param>
|
||||
/// <returns>True if the message is for the subscription which sent the request</returns>
|
||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
|
||||
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
||||
/// <param name="message">The received data</param>
|
||||
/// <param name="identifier">The string identifier of the handler</param>
|
||||
/// <returns>True if the message is for the handler which has the identifier</returns>
|
||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
|
||||
/// <summary>
|
||||
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection that should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected internal abstract Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socketConnection);
|
||||
/// <summary>
|
||||
/// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection on which to unsubscribe</param>
|
||||
/// <param name="subscriptionToUnsub">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
|
||||
|
||||
/// <summary>
|
||||
/// Optional handler to interpolate data before sending it to the handlers
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
protected internal virtual JToken ProcessTokenData(JToken message)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a subscription to a connection
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of data the subscription expects</typeparam>
|
||||
/// <param name="request">The request of the subscription</param>
|
||||
/// <param name="identifier">The identifier of the subscription (can be null if request param is used)</param>
|
||||
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
|
||||
/// <param name="connection">The socket connection the handler is on</param>
|
||||
/// <param name="dataHandler">The handler of the data received</param>
|
||||
/// <param name="authenticated">Whether the subscription needs authentication</param>
|
||||
/// <returns></returns>
|
||||
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
|
||||
{
|
||||
void InternalHandler(MessageEvent messageEvent)
|
||||
{
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
|
||||
dataHandler(new DataEvent<T>(stringData, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
return;
|
||||
}
|
||||
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||
if (!desResult)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
}
|
||||
|
||||
var subscription = request == null
|
||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||
if (!connection.AddSubscription(subscription))
|
||||
return null;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a generic message handler. Used for example to reply to ping requests
|
||||
/// </summary>
|
||||
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
|
||||
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
|
||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
||||
{
|
||||
genericHandlers.Add(identifier, action);
|
||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
|
||||
foreach (var connection in socketConnections.Values)
|
||||
connection.AddSubscription(subscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="authentication"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(SocketApiClient apiClient, string address, bool authentication)
|
||||
{
|
||||
return Task.FromResult(new CallResult<string?>(address));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The API client the connection is for</param>
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
||||
{
|
||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||
if (result != null)
|
||||
{
|
||||
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= ClientOptions.MaxSocketConnections && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
return new CallResult<SocketConnection>(result);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(apiClient, address, authenticated).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||
return connectionAddress.As<SocketConnection>(null);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
log.Write(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||
|
||||
// Create new socket
|
||||
var socket = CreateSocket(connectionAddress.Data!);
|
||||
var socketConnection = new SocketConnection(this, apiClient, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
foreach (var kvp in genericHandlers)
|
||||
{
|
||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
|
||||
socketConnection.AddSubscription(handler);
|
||||
}
|
||||
|
||||
return new CallResult<SocketConnection>(socketConnection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an unhandled message
|
||||
/// </summary>
|
||||
/// <param name="token">The token that wasn't processed</param>
|
||||
protected virtual void HandleUnhandledMessage(JToken token)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect a socket
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket to connect</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
||||
{
|
||||
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
||||
{
|
||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
socketConnection.Dispose();
|
||||
return new CallResult<bool>(new CantConnectError());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get parameters for the websocket connection
|
||||
/// </summary>
|
||||
/// <param name="address">The address to connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||
=> new (new Uri(address), ClientOptions.AutoReconnect)
|
||||
{
|
||||
DataInterpreterBytes = dataInterpreterBytes,
|
||||
DataInterpreterString = dataInterpreterString,
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ClientOptions.SocketNoDataTimeout
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a socket for an address
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket should connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IWebsocket CreateSocket(string address)
|
||||
{
|
||||
var socket = SocketFactory.CreateWebsocket(log, GetWebSocketParameters(address));
|
||||
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||
return socket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodically sends data over a socket connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">Identifier for the periodic send</param>
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
{
|
||||
if (objGetter == null)
|
||||
throw new ArgumentNullException(nameof(objGetter));
|
||||
|
||||
periodicEvent = new AsyncResetEvent();
|
||||
periodicTask = Task.Run(async () =>
|
||||
{
|
||||
while (!disposing)
|
||||
{
|
||||
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
|
||||
if (disposing)
|
||||
break;
|
||||
|
||||
foreach (var socketConnection in socketConnections.Values)
|
||||
{
|
||||
if (disposing)
|
||||
break;
|
||||
|
||||
if (!socketConnection.Connected)
|
||||
continue;
|
||||
|
||||
var obj = objGetter(socketConnection);
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
log.Write(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||
|
||||
try
|
||||
{
|
||||
socketConnection.Send(obj);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -697,23 +45,12 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
SocketSubscription? subscription = null;
|
||||
SocketConnection? connection = null;
|
||||
foreach(var socket in socketConnections.Values.ToList())
|
||||
foreach(var socket in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
subscription = socket.GetSubscription(subscriptionId);
|
||||
if (subscription != null)
|
||||
{
|
||||
connection = socket;
|
||||
break;
|
||||
}
|
||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||
if (result)
|
||||
break;
|
||||
}
|
||||
|
||||
if (subscription == null || connection == null)
|
||||
return;
|
||||
|
||||
log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
|
||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -726,7 +63,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
log.Write(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -736,14 +73,10 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
tasks.Add(client.UnsubscribeAllAsync());
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -753,14 +86,12 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.TriggerReconnectAsync());
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -769,32 +100,12 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
|
||||
foreach(var connection in socketConnections)
|
||||
var result = new StringBuilder();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
||||
{
|
||||
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
|
||||
foreach (var subscription in connection.Value.Subscriptions)
|
||||
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the client
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
disposing = true;
|
||||
periodicEvent?.Set();
|
||||
periodicEvent?.Dispose();
|
||||
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
_ = UnsubscribeAllAsync();
|
||||
}
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of the registered ISpotClient implementations
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<ISpotClient> GetSpotClients()
|
||||
{
|
||||
if (_serviceProvider == null)
|
||||
return new List<ISpotClient>();
|
||||
|
||||
return _serviceProvider.GetServices<ISpotClient>().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an ISpotClient implementation by exchange name
|
||||
/// </summary>
|
||||
/// <param name="exchangeName"></param>
|
||||
/// <returns></returns>
|
||||
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
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,74 +1,606 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client for interacting with a REST API
|
||||
/// </summary>
|
||||
public abstract class RestApiClient: BaseApiClient
|
||||
public abstract class RestApiClient : BaseApiClient, IRestApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Get time sync info for an API client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract TimeSyncInfo GetTimeSyncInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Get time offset for an API client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract TimeSpan GetTimeOffset();
|
||||
/// <inheritdoc />
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for this client
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
|
||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of rate limiters
|
||||
/// </summary>
|
||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="httpClient">HttpClient to use</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
|
||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
|
||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a request to the uri and returns if it was successful
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult> SendRequestAsync(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
int currentTry = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||
if (!result)
|
||||
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
|
||||
else
|
||||
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
|
||||
|
||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
return result.AsDataless();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false
|
||||
) where T : class
|
||||
{
|
||||
int currentTry = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult<T>(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||
if (!result)
|
||||
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
|
||||
else
|
||||
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
|
||||
|
||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a request to be sent to the server
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
|
||||
if (signed)
|
||||
{
|
||||
var syncTask = SyncTimeAsync();
|
||||
var timeSyncInfo = GetTimeSyncInfo();
|
||||
|
||||
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||
{
|
||||
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||
if (!syncTimeResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, $"[Req {requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
return syncTimeResult.As<IRequest>(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreRatelimit)
|
||||
{
|
||||
foreach (var limiter in RateLimiters)
|
||||
{
|
||||
var limitResult = await limiter.LimitRequestAsync(_logger, uri.AbsolutePath, method, signed, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, ApiOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult.Success)
|
||||
return new CallResult<IRequest>(limitResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (signed && AuthenticationProvider == null)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"[Req {requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Information, $"[Req {requestId}] Creating request for " + uri);
|
||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders);
|
||||
|
||||
string? paramString = "";
|
||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
|
||||
var headers = request.GetHeaders();
|
||||
if (headers.Any())
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
|
||||
TotalRequestsMade++;
|
||||
_logger.Log(LogLevel.Trace, $"[Req {requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
|
||||
return new CallResult<IRequest>(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request and returns the result deserialized into the type parameter class
|
||||
/// </summary>
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||
IRequest request,
|
||||
JsonSerializer? deserializer,
|
||||
CancellationToken cancellationToken,
|
||||
bool expectedEmptyResponse)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
var statusCode = response.StatusCode;
|
||||
var headers = response.ResponseHeaders;
|
||||
var responseLength = response.ContentLength;
|
||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
||||
// response before being able to deserialize it into the resulting type since we don't know if its an error response or data
|
||||
if (manualParseError)
|
||||
{
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
responseLength ??= data.Length;
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
if (!expectedEmptyResponse)
|
||||
{
|
||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
// Let the library implementation see if it is an error response, and if so parse the error
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
|
||||
// Not an error, so continue deserializing
|
||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data))
|
||||
{
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
// Not empty, and not json
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
// Error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
}
|
||||
|
||||
// Empty success response; okay
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expectedEmptyResponse)
|
||||
{
|
||||
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
}
|
||||
|
||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, responseLength, OutputOriginalData ? desResult.OriginalData : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Http status code indicates error
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
Error error;
|
||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
||||
else
|
||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
||||
|
||||
if (error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
|
||||
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
|
||||
/// If the response is an error this method should return the parsed error, else it should return null
|
||||
/// </summary>
|
||||
/// <param name="data">Received data</param>
|
||||
/// <returns>Null if not an error, Error otherwise</returns>
|
||||
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
||||
{
|
||||
return Task.FromResult<ServerError?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||
/// Note that this is always called; even when the request might be successful
|
||||
/// </summary>
|
||||
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
||||
/// <param name="callResult">The result of the call</param>
|
||||
/// <param name="tries">The current try number</param>
|
||||
/// <returns>True if call should retry, false if the call should return</returns>
|
||||
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request object
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
||||
/// <param name="bodyFormat">Format of the body content</param>
|
||||
/// <param name="requestId">Unique id of a request</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest ConstructRequest(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object>? parameters,
|
||||
bool signed,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
RequestBodyFormat bodyFormat,
|
||||
int requestId,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
|
||||
for (var i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var kvp = parameters.ElementAt(i);
|
||||
if (kvp.Value is Func<object> delegateValue)
|
||||
parameters[kvp.Key] = delegateValue();
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||
{
|
||||
foreach (var parameter in parameters)
|
||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||
if (AuthenticationProvider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
AuthenticationProvider.AuthenticateRequest(
|
||||
this,
|
||||
uri,
|
||||
method,
|
||||
parameters,
|
||||
signed,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
out uriParameters,
|
||||
out bodyParameters,
|
||||
out headers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||
{
|
||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||
$"should return provided parameters in either the uri or body parameters output");
|
||||
}
|
||||
}
|
||||
|
||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(method, uri, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
foreach (var header in additionalHeaders)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (StandardRequestHeaders != null)
|
||||
{
|
||||
foreach (var header in StandardRequestHeaders)
|
||||
{
|
||||
// Only add it if it isn't overwritten
|
||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
WriteParamBody(request, bodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(requestBodyEmptyContent, contentType);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the parameters of the request to the request object body
|
||||
/// </summary>
|
||||
/// <param name="request">The request to set the parameters on</param>
|
||||
/// <param name="parameters">The parameters to set</param>
|
||||
/// <param name="contentType">The content type of the data</param>
|
||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||
{
|
||||
if (contentType == Constants.JsonContentHeader)
|
||||
{
|
||||
// Write the parameters as json in the body
|
||||
var stringData = JsonConvert.SerializeObject(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
|
||||
/// </summary>
|
||||
/// <param name="httpStatusCode">The response status code</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <param name="data">The response data</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||
{
|
||||
return new ServerError(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
||||
/// </summary>
|
||||
/// <param name="httpStatusCode">The response status code</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <param name="data">The response data</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||
{
|
||||
// Handle retry after header
|
||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (retryAfterHeader.Value?.Any() != true)
|
||||
return new ServerRateLimitError(data);
|
||||
|
||||
var value = retryAfterHeader.Value.First();
|
||||
if (int.TryParse(value, out var seconds))
|
||||
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||
|
||||
if (DateTime.TryParse(value, out var datetime))
|
||||
return new ServerRateLimitError(data) { RetryAfter = datetime };
|
||||
|
||||
return new ServerRateLimitError(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
/// <returns>Server time</returns>
|
||||
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
|
||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
|
||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
@@ -94,10 +626,10 @@ namespace CryptoExchange.Net
|
||||
// Calculate time offset between local and server
|
||||
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
|
||||
timeSyncParams.UpdateTimeOffset(offset);
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,673 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base socket API client for interaction with a websocket API
|
||||
/// </summary>
|
||||
public abstract class SocketApiClient : BaseApiClient
|
||||
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
||||
{
|
||||
#region Fields
|
||||
/// <inheritdoc/>
|
||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||
|
||||
/// <summary>
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
/// </summary>
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
|
||||
/// <summary>
|
||||
/// Keep alive interval for websocket connection
|
||||
/// </summary>
|
||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||
/// </summary>
|
||||
protected List<SystemSubscription> systemSubscriptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
|
||||
/// </summary>
|
||||
protected internal bool UnhandledMessageExpected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true a subscription will accept message before the confirmation of a subscription has been received
|
||||
/// </summary>
|
||||
protected bool HandleMessageBeforeConfirmation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rate limiters
|
||||
/// </summary>
|
||||
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Periodic task regisrations
|
||||
/// </summary>
|
||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => socketConnections.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="logger">log</param>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a query to periodically send on each connection
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <param name="interval"></param>
|
||||
/// <param name="queryDelegate"></param>
|
||||
/// <param name="callback"></param>
|
||||
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
|
||||
{
|
||||
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
|
||||
{
|
||||
Identifier = identifier,
|
||||
Callback = callback,
|
||||
Interval = interval,
|
||||
QueryDelegate = queryDelegate
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to an url and listen for data on the BaseAddress
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
|
||||
{
|
||||
return SubscribeAsync(BaseAddress, subscription, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to an url and listen for data
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to connect to</param>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
if (subscription.Authenticated && AuthenticationProvider == null)
|
||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||
// This is necessary for being able to see if connections can be combined
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
var success = socketConnection.CanAddSubscription();
|
||||
if (!success)
|
||||
{
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {socketConnection.SocketId}] failed to add subscription, retrying on different connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't subscribe at this moment");
|
||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
||||
if (subQuery != null)
|
||||
{
|
||||
if (HandleMessageBeforeConfirmation)
|
||||
socketConnection.AddSubscription(subscription);
|
||||
|
||||
// Send the request and wait for answer
|
||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
waitEvent?.Set();
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}");
|
||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||
var unsubscribe = subResult.Error is CancellationRequestedError;
|
||||
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
||||
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
}
|
||||
|
||||
subscription.Confirmed = true;
|
||||
if (ct != default)
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] Cancellation token set, closing subscription {subscription.Id}");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}, false);
|
||||
}
|
||||
|
||||
if (!HandleMessageBeforeConfirmation)
|
||||
socketConnection.AddSubscription(subscription);
|
||||
|
||||
waitEvent?.Set();
|
||||
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] subscription {subscription.Id} completed successfully");
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Expected result type</typeparam>
|
||||
/// <param name="query">The query</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query)
|
||||
{
|
||||
return QueryAsync(BaseAddress, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a query on a socket connection and wait for the response
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expected result type</typeparam>
|
||||
/// <param name="url">The url for the request</param>
|
||||
/// <param name="query">The query</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<T>(default);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Can release early when only a single sub per connection
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<T>(connectResult.Error!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't send query at this moment");
|
||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
|
||||
/// </summary>
|
||||
/// <param name="socket">The connection to check</param>
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<bool>(connectResult.Error!);
|
||||
|
||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <param name="socket">Socket to authenticate</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socket)
|
||||
{
|
||||
if (AuthenticationProvider == null)
|
||||
return new CallResult<bool>(new NoApiCredentialsError());
|
||||
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] Attempting to authenticate");
|
||||
var authRequest = GetAuthenticationRequest();
|
||||
if (authRequest != null)
|
||||
{
|
||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {socket.SocketId}] authentication failed");
|
||||
if (socket.Connected)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult<bool>(result.Error)!;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] authenticated");
|
||||
socket.Authenticated = true;
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
/// </summary>
|
||||
/// <param name="systemSubscription">The subscription</param>
|
||||
protected void AddSystemSubscription(SystemSubscription systemSubscription)
|
||||
{
|
||||
systemSubscriptions.Add(systemSubscription);
|
||||
foreach (var connection in socketConnections.Values)
|
||||
connection.AddSubscription(systemSubscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="authentication"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
|
||||
{
|
||||
return Task.FromResult(new CallResult<string?>(address));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to reconnect to after losing a connection
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||
{
|
||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
||||
{
|
||||
return Task.FromResult(new CallResult(null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
||||
{
|
||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& (s.Value.ApiClient.GetType() == GetType())
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
|
||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||
if (result != null)
|
||||
{
|
||||
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
return new CallResult<SocketConnection>(result);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||
return connectionAddress.As<SocketConnection>(null);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
_logger.Log(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||
|
||||
// Create new socket
|
||||
var socket = CreateSocket(connectionAddress.Data!);
|
||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
|
||||
foreach (var ptg in PeriodicTaskRegistrations)
|
||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||
|
||||
foreach (var systemSubscription in systemSubscriptions)
|
||||
socketConnection.AddSubscription(systemSubscription);
|
||||
|
||||
return new CallResult<SocketConnection>(socketConnection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an unhandled message
|
||||
/// </summary>
|
||||
/// <param name="message">The message that wasn't processed</param>
|
||||
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect a socket
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket to connect</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
||||
{
|
||||
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
||||
{
|
||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
socketConnection.Dispose();
|
||||
return new CallResult<bool>(new CantConnectError());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get parameters for the websocket connection
|
||||
/// </summary>
|
||||
/// <param name="address">The address to connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
||||
{
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RateLimiters = RateLimiters,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a socket for an address
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket should connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IWebsocket CreateSocket(string address)
|
||||
{
|
||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {socket.Id}] created for " + address);
|
||||
return socket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<bool> UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
Subscription? subscription = null;
|
||||
SocketConnection? connection = null;
|
||||
foreach (var socket in socketConnections.Values.ToList())
|
||||
{
|
||||
subscription = socket.GetSubscription(subscriptionId);
|
||||
if (subscription != null)
|
||||
{
|
||||
connection = socket;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription == null || connection == null)
|
||||
return false;
|
||||
|
||||
_logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] unsubscribing subscription " + subscriptionId);
|
||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
_logger.Log(LogLevel.Information, $"[Sckt {subscription.SocketId}] Unsubscribing subscription " + subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
|
||||
if (sum == 0)
|
||||
return;
|
||||
|
||||
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.UserSubscriptionCount)} subscriptions");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.TriggerReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{GetType().Name}");
|
||||
sb.AppendLine($" Connections: {socketConnections.Count}");
|
||||
sb.AppendLine($" Subscriptions: {CurrentSubscriptions}");
|
||||
sb.AppendLine($" Download speed: {IncomingKbps} kbps");
|
||||
foreach (var connection in socketConnections)
|
||||
{
|
||||
sb.AppendLine($" Id: {connection.Key}");
|
||||
sb.AppendLine($" Address: {connection.Value.ConnectionUri}");
|
||||
sb.AppendLine($" Subscriptions: {connection.Value.UserSubscriptionCount}");
|
||||
sb.AppendLine($" Status: {connection.Value.Status}");
|
||||
sb.AppendLine($" Authenticated: {connection.Value.Authenticated}");
|
||||
sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps");
|
||||
sb.AppendLine($" Subscriptions:");
|
||||
foreach (var subscription in connection.Value.Subscriptions)
|
||||
{
|
||||
sb.AppendLine($" Id: {subscription.Id}");
|
||||
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
|
||||
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
|
||||
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the client
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
_ = UnsubscribeAllAsync();
|
||||
}
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the listener identifier for the message
|
||||
/// </summary>
|
||||
/// <param name="messageAccessor"></param>
|
||||
/// <returns></returns>
|
||||
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
|
||||
|
||||
/// <summary>
|
||||
/// Preprocess a stream message
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) => stream;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order type
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of an order
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticker data
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
public class ArrayConverter : JsonConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
@@ -28,6 +28,9 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
if (objectType == typeof(JToken))
|
||||
return JToken.Load(reader);
|
||||
|
||||
@@ -97,18 +100,30 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
if (value != null && property.PropertyType.IsInstanceOfType(value))
|
||||
{
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value is JToken token)
|
||||
{
|
||||
if (token.Type == JTokenType.Null)
|
||||
value = null;
|
||||
|
||||
if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().Contains("e")))
|
||||
if (token.Type == JTokenType.Float)
|
||||
value = token.Value<decimal>();
|
||||
}
|
||||
|
||||
if (value is decimal)
|
||||
{
|
||||
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
var v = value.ToString();
|
||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
property.SetValue(result, dec);
|
||||
}
|
||||
else
|
||||
@@ -172,10 +187,10 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||
(T?)attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
|
||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||
(T?)attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// The enum->string mapping
|
||||
/// </summary>
|
||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
||||
private readonly bool quotes;
|
||||
private readonly bool _quotes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="useQuotes"></param>
|
||||
protected BaseConverter(bool useQuotes)
|
||||
{
|
||||
quotes = useQuotes;
|
||||
_quotes = useQuotes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = value == null? null: GetValue((T) value);
|
||||
if (quotes)
|
||||
if (_quotes)
|
||||
writer.WriteValue(stringValue);
|
||||
else
|
||||
writer.WriteRawValue(stringValue);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Boolean converter with support for "0"/"1" (strings)
|
||||
/// </summary>
|
||||
public class BoolConverter : JsonConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this instance can convert the specified object type.
|
||||
/// </summary>
|
||||
/// <param name="objectType">Type of the object.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
|
||||
return objectType == typeof(bool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the JSON representation of the object.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
|
||||
/// <param name="objectType">Type of the object.</param>
|
||||
/// <param name="existingValue">The existing value of object being read.</param>
|
||||
/// <param name="serializer">The calling serializer.</param>
|
||||
/// <returns>
|
||||
/// The object value.
|
||||
/// </returns>
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var value = reader.Value?.ToString().ToLower().Trim();
|
||||
if (value == null || value == "")
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
case "0":
|
||||
case "off":
|
||||
case "-1":
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
|
||||
return new JsonSerializer().Deserialize(reader, objectType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that this converter will not participate in writing results.
|
||||
/// </summary>
|
||||
public override bool CanWrite { get { return false; } }
|
||||
|
||||
/// <summary>
|
||||
/// Writes the JSON representation of the object.
|
||||
/// </summary>
|
||||
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ namespace CryptoExchange.Net.Converters
|
||||
public class DateTimeConverter: JsonConverter
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
@@ -59,8 +59,12 @@ namespace CryptoExchange.Net.Converters
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue == "-1"
|
||||
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
||||
{
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
}
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
@@ -134,7 +138,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * ticksPerSecond));
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
@@ -146,13 +150,13 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
/// <param name="microseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * ticksPerMicrosecond));
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="nanoseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * ticksPerNanosecond));
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
@@ -173,14 +177,14 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerMicrosecond);
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerNanosecond);
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Kraken.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for serializing decimal values as string
|
||||
/// </summary>
|
||||
public class DecimalStringWriterConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,29 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
public class EnumConverter : JsonConverter
|
||||
{
|
||||
private bool _warnOnMissingEntry = true;
|
||||
private bool _writeAsInt;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public EnumConverter() { }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="writeAsInt"></param>
|
||||
/// <param name="warnOnMissingEntry"></param>
|
||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
||||
{
|
||||
_writeAsInt = writeAsInt;
|
||||
_warnOnMissingEntry = warnOnMissingEntry;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsEnum;
|
||||
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -30,7 +47,7 @@ namespace CryptoExchange.Net.Converters
|
||||
mapping = AddMapping(enumType);
|
||||
|
||||
var stringValue = reader.Value?.ToString();
|
||||
if (stringValue == null)
|
||||
if (stringValue == null || stringValue == "")
|
||||
{
|
||||
// Received null value
|
||||
var emptyResult = GetDefaultValue(objectType, enumType);
|
||||
@@ -51,8 +68,12 @@ namespace CryptoExchange.Net.Converters
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
{
|
||||
// We received an enum value but weren't able to parse it.
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
||||
if (_warnOnMissingEntry)
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -117,22 +138,39 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString<T>(T enumValue)
|
||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
private static string? GetString(Type objectType, object? enumValue)
|
||||
{
|
||||
var objectType = typeof(T);
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = GetString(value);
|
||||
writer.WriteRawValue(stringValue);
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_writeAsInt)
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteValue(stringValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteValue((int)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer options
|
||||
/// </summary>
|
||||
public static class SerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
Converters =
|
||||
{
|
||||
new EnumConverter(),
|
||||
new DateTimeConverter(),
|
||||
new BoolConverter()
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Default json serializer settings
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings Default => new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,20 +6,26 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>5.2.1</PackageVersion>
|
||||
<AssemblyVersion>5.2.1</AssemblyVersion>
|
||||
<FileVersion>5.2.1</FileVersion>
|
||||
<PackageVersion>7.0.0-beta2</PackageVersion>
|
||||
<AssemblyVersion>7.0.0-beta2</AssemblyVersion>
|
||||
<FileVersion>7.0.0-beta2</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>5.2.1 - Fixed socket reconnect issue, Fixed `message not handled` messages after unsubscribing, Fixed error returning for non-json error responses</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>7.0.0-beta2 - Updated RevitalizeRequestAsync signature, Removed duplicate logging</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
@@ -41,14 +47,15 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -8,6 +9,17 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class ExchangeHelpers
|
||||
{
|
||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
private static int _lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
private static object _idLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a value between a min and max
|
||||
/// </summary>
|
||||
@@ -43,7 +55,9 @@ namespace CryptoExchange.Net
|
||||
|
||||
var offset = value % step.Value;
|
||||
if(roundingType == RoundingType.Down)
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
@@ -116,5 +130,66 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
return value / 1.000000000000000000000000000000000m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int NextId()
|
||||
{
|
||||
lock (_idLock)
|
||||
{
|
||||
_lastId += 1;
|
||||
return _lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the last unique id that was generated
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int LastId()
|
||||
{
|
||||
lock (_idLock)
|
||||
return _lastId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="length">Length of the random string</param>
|
||||
/// <returns></returns>
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
var randomChars = new char[length];
|
||||
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||
#else
|
||||
var random = new Random();
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
||||
#endif
|
||||
|
||||
return new string(randomChars);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="source">The initial string</param>
|
||||
/// <param name="totalLength">Total length of the resulting string</param>
|
||||
/// <returns></returns>
|
||||
public static string AppendRandomString(string source, int totalLength)
|
||||
{
|
||||
if (totalLength < source.Length)
|
||||
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
||||
|
||||
if (totalLength == source.Length)
|
||||
return source;
|
||||
|
||||
return source + RandomString(totalLength - source.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -90,31 +89,6 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="converter"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value, JsonConverter converter)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a query string of the specified parameters
|
||||
/// </summary>
|
||||
@@ -129,7 +103,9 @@ namespace CryptoExchange.Net
|
||||
foreach (var arrayEntry in arraysParameters)
|
||||
{
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
}
|
||||
else
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
@@ -160,7 +136,9 @@ namespace CryptoExchange.Net
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
}
|
||||
return formData.ToString();
|
||||
}
|
||||
@@ -224,7 +202,11 @@ namespace CryptoExchange.Net
|
||||
if (b1 != b2) return false;
|
||||
}
|
||||
}
|
||||
else return false;
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
@@ -252,9 +234,9 @@ namespace CryptoExchange.Net
|
||||
/// String to JToken
|
||||
/// </summary>
|
||||
/// <param name="stringData"></param>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static JToken? ToJToken(this string stringData, Log? log = null)
|
||||
public static JToken? ToJToken(this string stringData, ILogger? logger = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stringData))
|
||||
return null;
|
||||
@@ -266,15 +248,15 @@ namespace CryptoExchange.Net
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -288,8 +270,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||
{
|
||||
if (!allowedValues.Contains(value))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -302,8 +286,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||
{
|
||||
if (value < minValue || value > maxValue)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -437,7 +423,9 @@ namespace CryptoExchange.Net
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
@@ -466,13 +454,14 @@ namespace CryptoExchange.Net
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add parameter to URI
|
||||
/// </summary>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -2,7 +2,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Base api client
|
||||
/// </summary>
|
||||
public interface IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Base address
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for accessing REST API's for different exchanges
|
||||
/// </summary>
|
||||
public interface ICryptoRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a list of all registered common ISpotClient types
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<ISpotClient> GetSpotClients();
|
||||
|
||||
/// <summary>
|
||||
/// Get an ISpotClient implementation by exchange name
|
||||
/// </summary>
|
||||
/// <param name="exchangeName"></param>
|
||||
/// <returns></returns>
|
||||
ISpotClient? SpotClient(string exchangeName);
|
||||
|
||||
/// <summary>
|
||||
/// Try get
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
T TryGet<T>(Func<T> createFunc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Message processor
|
||||
/// </summary>
|
||||
public interface IMessageProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the processor
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
/// <summary>
|
||||
/// The identifiers for this processor
|
||||
/// </summary>
|
||||
public HashSet<string> ListenerIdentifiers { get; }
|
||||
/// <summary>
|
||||
/// Handle a message
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
|
||||
/// <summary>
|
||||
/// Get the type the message should be deserialized to
|
||||
/// </summary>
|
||||
/// <param name="messageAccessor"></param>
|
||||
/// <returns></returns>
|
||||
Type? GetMessageType(IMessageAccessor messageAccessor);
|
||||
/// <summary>
|
||||
/// Deserialize a message int oobject of type
|
||||
/// </summary>
|
||||
/// <param name="accessor"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
object Deserialize(IMessageAccessor accessor, Type type);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
@@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="requestWeight">The weight of the request</param>
|
||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||
/// <returns>The time in milliseconds spend waiting</returns>
|
||||
Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Configure the requests created by this factory
|
||||
/// </summary>
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
/// <param name="proxy">Proxy settings to use</param>
|
||||
/// <param name="httpClient">Optional shared http client instance</param>
|
||||
void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? httpClient=null);
|
||||
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
bool IsSuccessStatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the response in bytes
|
||||
/// </summary>
|
||||
long? ContentLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client
|
||||
/// </summary>
|
||||
public interface IRestApiClient : IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating requests. Used for unit testing
|
||||
/// </summary>
|
||||
IRequestFactory RequestFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -10,24 +9,13 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface IRestClient: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating requests. Used for unit testing
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
IRequestFactory RequestFactory { get; set; }
|
||||
ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of requests made with this client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
BaseRestClientOptions ClientOptions { get; }
|
||||
|
||||
/// <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(ApiCredentials credentials);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket API client
|
||||
/// </summary>
|
||||
public interface ISocketApiClient: IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The current amount of socket connections on the API client
|
||||
/// </summary>
|
||||
int CurrentConnections { get; }
|
||||
/// <summary>
|
||||
/// The current amount of subscriptions over all connections
|
||||
/// </summary>
|
||||
int CurrentSubscriptions { get; }
|
||||
/// <summary>
|
||||
/// Incoming data kpbs
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
/// <summary>
|
||||
/// Current client options
|
||||
/// </summary>
|
||||
SocketExchangeOptions ClientOptions { get; }
|
||||
/// <summary>
|
||||
/// Current API options
|
||||
/// </summary>
|
||||
SocketApiOptions ApiOptions { get; }
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
string GetSubscriptionsState();
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ReconnectAsync();
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAllAsync();
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UnsubscribeAsync(int subscriptionId);
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -14,13 +13,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
BaseSocketClientOptions ClientOptions { get; }
|
||||
|
||||
/// <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(ApiCredentials credentials);
|
||||
ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Incoming kilobytes per second of data
|
||||
|
||||
@@ -101,13 +101,22 @@ namespace CryptoExchange.Net.Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
|
||||
/// at that price since between this calculation and the order placement the book can have changed.
|
||||
/// at that price since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quantity">The quantity in base asset to fill</param>
|
||||
/// <param name="type">The type</param>
|
||||
/// <returns>Average fill price</returns>
|
||||
CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type);
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
|
||||
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
|
||||
/// <param name="type">The type</param>
|
||||
/// <returns>Amount of base asset tradable with the specified amount of quote asset</returns>
|
||||
CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type);
|
||||
|
||||
/// <summary>
|
||||
/// String representation of the top x entries
|
||||
/// </summary>
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Webscoket connection interface
|
||||
/// Websocket connection interface
|
||||
/// </summary>
|
||||
public interface IWebsocket: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Websocket closed event
|
||||
/// </summary>
|
||||
event Action OnClose;
|
||||
event Func<Task> OnClose;
|
||||
/// <summary>
|
||||
/// Websocket message received event
|
||||
/// </summary>
|
||||
event Action<string> OnMessage;
|
||||
event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
|
||||
/// <summary>
|
||||
/// Websocket sent event, RequestId as parameter
|
||||
/// </summary>
|
||||
event Func<int, Task> OnRequestSent;
|
||||
/// <summary>
|
||||
/// Websocket error event
|
||||
/// </summary>
|
||||
event Action<Exception> OnError;
|
||||
event Func<Exception, Task> OnError;
|
||||
/// <summary>
|
||||
/// Websocket opened event
|
||||
/// </summary>
|
||||
event Action OnOpen;
|
||||
event Func<Task> OnOpen;
|
||||
/// <summary>
|
||||
/// Websocket has lost connection to the server and is attempting to reconnect
|
||||
/// </summary>
|
||||
event Action OnReconnecting;
|
||||
event Func<Task> OnReconnecting;
|
||||
/// <summary>
|
||||
/// Websocket has reconnected to the server
|
||||
/// </summary>
|
||||
event Action OnReconnected;
|
||||
event Func<Task> OnReconnected;
|
||||
/// <summary>
|
||||
/// Get reconntion url
|
||||
/// </summary>
|
||||
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique id for this socket
|
||||
@@ -64,8 +71,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
void Send(string data);
|
||||
/// <param name="weight"></param>
|
||||
void Send(int id, string data, int weight);
|
||||
/// <summary>
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -11,9 +11,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="logger">The logger</param>
|
||||
/// <param name="parameters">The parameters to use for the connection</param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters);
|
||||
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// ILogger implementation for logging to the console
|
||||
/// </summary>
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Console.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Default log writer, uses Trace.WriteLine
|
||||
/// </summary>
|
||||
public class DebugLogger: ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Log implementation
|
||||
/// </summary>
|
||||
public class Log
|
||||
{
|
||||
/// <summary>
|
||||
/// List of ILogger implementations to forward the message to
|
||||
/// </summary>
|
||||
private List<ILogger> writers;
|
||||
|
||||
/// <summary>
|
||||
/// The verbosity of the logging, anything more verbose will not be forwarded to the writers
|
||||
/// </summary>
|
||||
public LogLevel? Level { get; set; } = LogLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="clientName">The name of the client the logging is used in</param>
|
||||
public Log(string clientName)
|
||||
{
|
||||
ClientName = clientName;
|
||||
writers = new List<ILogger>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the writers
|
||||
/// </summary>
|
||||
/// <param name="textWriters"></param>
|
||||
public void UpdateWriters(List<ILogger> textWriters)
|
||||
{
|
||||
lock (_lock)
|
||||
writers = textWriters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a log entry
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The verbosity of the message</param>
|
||||
/// <param name="message">The message to log</param>
|
||||
public void Write(LogLevel logLevel, string message)
|
||||
{
|
||||
if (Level != null && (int)logLevel < (int)Level)
|
||||
return;
|
||||
|
||||
var logMessage = $"{ClientName,-10} | {message}";
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var writer in writers)
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.Log(logLevel, logMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Can't write to the logging so where else to output..
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -12,7 +13,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public class AsyncResetEvent : IDisposable
|
||||
{
|
||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||
private readonly Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
private bool _signaled;
|
||||
private readonly bool _reset;
|
||||
|
||||
@@ -49,7 +50,13 @@ namespace CryptoExchange.Net.Objects
|
||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
||||
var registration = cancellationSource.Token.Register(() =>
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
lock (_waits)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
|
||||
// Not the cleanest but it works
|
||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||
}
|
||||
}, useSynchronizationContext: false);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@@ -38,6 +39,12 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +70,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="error"></param>
|
||||
#pragma warning disable 8618
|
||||
protected CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
|
||||
public CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
|
||||
#pragma warning restore 8618
|
||||
{
|
||||
OriginalData = originalData;
|
||||
@@ -84,6 +91,13 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="error">The erro rto return</param>
|
||||
public CallResult(Error error) : this(default, null, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error to return</param>
|
||||
/// <param name="originalData">The original response data</param>
|
||||
public CallResult(Error error, string? originalData) : this(default, originalData, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
|
||||
/// </summary>
|
||||
@@ -128,6 +142,24 @@ namespace CryptoExchange.Net.Objects
|
||||
return new CallResult<K>(data, OriginalData, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDataless()
|
||||
{
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new CallResult(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@@ -138,6 +170,12 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -155,6 +193,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
@@ -186,6 +229,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
@@ -195,6 +239,7 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
@@ -204,6 +249,7 @@ namespace CryptoExchange.Net.Objects
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
RequestId = requestId;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
@@ -224,7 +270,13 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +296,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
@@ -259,6 +316,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public HttpStatusCode? ResponseStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes of the response
|
||||
/// </summary>
|
||||
public long? ResponseLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
@@ -275,7 +337,9 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="responseLength"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
@@ -286,7 +350,9 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
int? requestId,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
@@ -297,18 +363,37 @@ namespace CryptoExchange.Net.Objects
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
ResponseLength = responseLength;
|
||||
|
||||
RequestId = requestId;
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, default, error) { }
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
@@ -318,25 +403,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -347,7 +414,20 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Success ? $"Success response" : $"Error response: {Error}");
|
||||
if (ResponseLength != null)
|
||||
sb.Append($", {ResponseLength} bytes");
|
||||
if (ResponseTime != null)
|
||||
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +124,19 @@
|
||||
/// </summary>
|
||||
Closest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of the update
|
||||
/// </summary>
|
||||
public enum SocketUpdateType
|
||||
{
|
||||
/// <summary>
|
||||
/// A update
|
||||
/// </summary>
|
||||
Update,
|
||||
/// <summary>
|
||||
/// A snapshot, generally send at the start of the connection
|
||||
/// </summary>
|
||||
Snapshot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace CryptoExchange.Net.Objects
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for errors
|
||||
@@ -39,7 +41,7 @@
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Code}: {Message} {Data}";
|
||||
return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +54,14 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CantConnectError() : base(null, "Can't connect to the server", null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,12 +73,20 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error returned by the server
|
||||
/// </summary>
|
||||
public class ServerError: Error
|
||||
public class ServerError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -83,9 +101,15 @@
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public ServerError(int code, string message, object? data = null) : base(code, message, data)
|
||||
{
|
||||
}
|
||||
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -107,6 +131,14 @@
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected WebError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,6 +152,14 @@
|
||||
/// <param name="message">The error message</param>
|
||||
/// <param name="data">The data which caused the error</param>
|
||||
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -133,6 +173,14 @@
|
||||
/// <param name="message">Error message</param>
|
||||
/// <param name="data">Error data</param>
|
||||
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,18 +193,73 @@
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit exceeded
|
||||
/// Rate limit exceeded (client side)
|
||||
/// </summary>
|
||||
public class RateLimitError: Error
|
||||
public abstract class BaseRateLimitError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// When the request can be retried
|
||||
/// </summary>
|
||||
public DateTime? RetryAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit exceeded (client side)
|
||||
/// </summary>
|
||||
public class ClientRateLimitError : BaseRateLimitError
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { }
|
||||
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit exceeded (server side)
|
||||
/// </summary>
|
||||
public class ServerRateLimitError : BaseRateLimitError
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -168,17 +271,33 @@
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalid operation requested
|
||||
/// </summary>
|
||||
public class InvalidOperationError: Error
|
||||
public class InvalidOperationError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public InvalidOperationError(string message) : base(null, message, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="data"></param>
|
||||
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base options, applicable to everything
|
||||
/// </summary>
|
||||
public class BaseOptions
|
||||
{
|
||||
internal event Action? OnLoggingChanged;
|
||||
|
||||
private LogLevel _logLevel = LogLevel.Information;
|
||||
/// <summary>
|
||||
/// The minimum log level to output
|
||||
/// </summary>
|
||||
public LogLevel LogLevel
|
||||
{
|
||||
get => _logLevel;
|
||||
set
|
||||
{
|
||||
_logLevel = value;
|
||||
OnLoggingChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
||||
/// <summary>
|
||||
/// The log writers
|
||||
/// </summary>
|
||||
public List<ILogger> LogWriters
|
||||
{
|
||||
get => _logWriters;
|
||||
set
|
||||
{
|
||||
_logWriters = value;
|
||||
OnLoggingChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public BaseOptions(): this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
||||
public BaseOptions(BaseOptions? baseOptions)
|
||||
{
|
||||
if (baseOptions == null)
|
||||
return;
|
||||
|
||||
LogLevel = baseOptions.LogLevel;
|
||||
LogWriters = baseOptions.LogWriters.ToList();
|
||||
OutputOriginalData = baseOptions.OutputOriginalData;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, OutputOriginalData: {OutputOriginalData}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client options, for both the socket and rest clients
|
||||
/// </summary>
|
||||
public class BaseClientOptions : BaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy to use when connecting
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api credentials to be used for signing requests to private endpoints. These credentials will be used for each API in the client, unless overriden in the API options
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public BaseClientOptions() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
||||
public BaseClientOptions(BaseClientOptions? baseOptions) : base(baseOptions)
|
||||
{
|
||||
if (baseOptions == null)
|
||||
return;
|
||||
|
||||
Proxy = baseOptions.Proxy;
|
||||
ApiCredentials = baseOptions.ApiCredentials?.Copy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}, Base.ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rest client options
|
||||
/// </summary>
|
||||
public class BaseRestClientOptions : BaseClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The time the server has to respond to a request before timing out
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
|
||||
/// </summary>
|
||||
public HttpClient? HttpClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public BaseRestClientOptions(): this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
||||
public BaseRestClientOptions(BaseRestClientOptions? baseOptions): base(baseOptions)
|
||||
{
|
||||
if (baseOptions == null)
|
||||
return;
|
||||
|
||||
HttpClient = baseOptions.HttpClient;
|
||||
RequestTimeout = baseOptions.RequestTimeout;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Socket client options
|
||||
/// </summary>
|
||||
public class BaseSocketClientOptions : BaseClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The max time to wait for a response after sending a request on the socket before giving a timeout
|
||||
/// </summary>
|
||||
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public BaseSocketClientOptions(): this(null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy options from these options to the new options</param>
|
||||
public BaseSocketClientOptions(BaseSocketClientOptions? baseOptions): base(baseOptions)
|
||||
{
|
||||
if (baseOptions == null)
|
||||
return;
|
||||
|
||||
AutoReconnect = baseOptions.AutoReconnect;
|
||||
ReconnectInterval = baseOptions.ReconnectInterval;
|
||||
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
|
||||
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
|
||||
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
|
||||
MaxSocketConnections = baseOptions.MaxSocketConnections;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API client options
|
||||
/// </summary>
|
||||
public class ApiClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The base address of the API
|
||||
/// </summary>
|
||||
public string BaseAddress { 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; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
#pragma warning disable 8618 // Will always get filled by the implementation
|
||||
public ApiClientOptions()
|
||||
{
|
||||
}
|
||||
#pragma warning restore 8618
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Base address for the API</param>
|
||||
public ApiClientOptions(string baseAddress)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOptions">Copy values for the provided options</param>
|
||||
/// <param name="newValues">Copy values for the provided options</param>
|
||||
public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
|
||||
{
|
||||
BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
|
||||
ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rest API client options
|
||||
/// </summary>
|
||||
public class RestApiClientOptions: ApiClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when a call would exceed the rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RestApiClientOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Base address for the API</param>
|
||||
public RestApiClientOptions(string baseAddress): base(baseAddress)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseOn">Copy values for the provided options</param>
|
||||
/// <param name="newValues">Copy values for the provided options</param>
|
||||
public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
|
||||
{
|
||||
RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
|
||||
AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
|
||||
TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
|
||||
RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : BaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for API usage
|
||||
/// </summary>
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool? OutputOriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange options
|
||||
/// </summary>
|
||||
public class ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy settings
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The max time a request is allowed to take
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : OrderBookOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Http api options
|
||||
/// </summary>
|
||||
public class RestApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when a call would exceed the rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool? AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan? TimestampRecalculationInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public virtual T Copy<T>() where T : RestApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
RateLimiters = RateLimiters,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
public class RestExchangeOptions: ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket api options
|
||||
/// </summary>
|
||||
public class SocketApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan? SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
RateLimiters = RateLimiters,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a websocket exchange client
|
||||
/// </summary>
|
||||
public class SocketExchangeOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
/// </summary>
|
||||
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoReconnect = AutoReconnect,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters collection
|
||||
/// </summary>
|
||||
public class ParameterCollection : Dictionary<string, object>
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptional(string key, object? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, decimal value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a decimal value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, decimal? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, int value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, int? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value as string
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddString(string key, long value)
|
||||
{
|
||||
Add(key, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a long value as string. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalString(string key, long? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddMilliseconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalMilliseconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddMillisecondsString(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as milliseconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalMillisecondsString(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as seconds timestamp
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddSeconds(string key, DateTime value)
|
||||
{
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a datetime value as seconds timestamp. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalSeconds(string key, DateTime? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddEnum<T>(string key, T value)
|
||||
{
|
||||
Add(key, EnumConverter.GetString(value)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public void AddOptionalEnum<T>(string key, T? value)
|
||||
{
|
||||
if (value != null)
|
||||
Add(key, EnumConverter.GetString(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -18,7 +17,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public class RateLimiter : IRateLimiter
|
||||
{
|
||||
private readonly object _limiterLock = new object();
|
||||
internal List<Limiter> Limiters = new List<Limiter>();
|
||||
internal List<Limiter> _limiters = new List<Limiter>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
||||
@@ -36,7 +35,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -82,7 +81,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
||||
_limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -96,33 +95,46 @@ namespace CryptoExchange.Net.Objects
|
||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
||||
_limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit for the amount of messages that can be send per connection
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint that the limit is for</param>
|
||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||
public RateLimiter AddConnectionRateLimit(string endpoint, int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
lock (_limiterLock)
|
||||
_limiters.Add(new ConnectionRateLimiter(new[] { endpoint }, limit, perTimePeriod));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
||||
public async Task<CallResult<int>> LimitRequestAsync(ILogger logger, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
||||
{
|
||||
int totalWaitTime = 0;
|
||||
|
||||
EndpointRateLimiter? endpointLimit;
|
||||
List<EndpointRateLimiter> endpointLimits;
|
||||
lock (_limiterLock)
|
||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||
if(endpointLimit != null)
|
||||
endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
|
||||
foreach (var endpointLimit in endpointLimits)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
if (endpointLimit?.IgnoreOtherRateLimits == true)
|
||||
if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||
lock (_limiterLock)
|
||||
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
||||
partialEndpointLimits = _limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
||||
foreach (var partialEndpointLimit in partialEndpointLimits)
|
||||
{
|
||||
if (partialEndpointLimit.CountPerEndpoint)
|
||||
@@ -130,15 +142,15 @@ namespace CryptoExchange.Net.Objects
|
||||
SingleTopicRateLimiter? thisEndpointLimit;
|
||||
lock (_limiterLock)
|
||||
{
|
||||
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
||||
thisEndpointLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
||||
if (thisEndpointLimit == null)
|
||||
{
|
||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
||||
Limiters.Add(thisEndpointLimit);
|
||||
_limiters.Add(thisEndpointLimit);
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(log, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@@ -146,7 +158,7 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
else
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@@ -157,16 +169,16 @@ namespace CryptoExchange.Net.Objects
|
||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
ApiKeyRateLimiter? apiLimit;
|
||||
List<ApiKeyRateLimiter> apiLimits;
|
||||
lock (_limiterLock)
|
||||
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
||||
if (apiLimit != null)
|
||||
apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
|
||||
foreach (var apiLimit in apiLimits)
|
||||
{
|
||||
if(apiKey == null)
|
||||
{
|
||||
if (!apiLimit.OnlyForSignedRequests)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@@ -178,15 +190,15 @@ namespace CryptoExchange.Net.Objects
|
||||
SingleTopicRateLimiter? thisApiLimit;
|
||||
lock (_limiterLock)
|
||||
{
|
||||
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
||||
thisApiLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
||||
if (thisApiLimit == null)
|
||||
{
|
||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
||||
Limiters.Add(thisApiLimit);
|
||||
_limiters.Add(thisApiLimit);
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(log, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@@ -194,15 +206,15 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
|
||||
if ((signed || apiLimits.All(l => !l.OnlyForSignedRequests)) && apiLimits.Any(l => l.IgnoreTotalRateLimit))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
TotalRateLimiter? totalLimit;
|
||||
List<TotalRateLimiter> totalLimits;
|
||||
lock (_limiterLock)
|
||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||
if (totalLimit != null)
|
||||
totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
|
||||
foreach(var totalLimit in totalLimits)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
@@ -212,7 +224,7 @@ namespace CryptoExchange.Net.Objects
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
|
||||
private static async Task<CallResult<int>> ProcessTopic(Log log, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
||||
private static async Task<CallResult<int>> ProcessTopic(ILogger logger, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
@@ -225,63 +237,68 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
int totalWaitTime = 0;
|
||||
while (true)
|
||||
try
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||
int totalWaitTime = 0;
|
||||
while (true)
|
||||
{
|
||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||
{
|
||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||
i--;
|
||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||
{
|
||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||
i--;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
|
||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||
{
|
||||
if (currentWeight == 0)
|
||||
throw new Exception("Request limit reached without any prior request. " +
|
||||
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
||||
|
||||
// Wait until the next entry should be removed from the history
|
||||
var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
|
||||
if (thisWaitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||
logger.Log(LogLevel.Warning, msg);
|
||||
return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
|
||||
}
|
||||
|
||||
logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
totalWaitTime += thisWaitTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
|
||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||
{
|
||||
if (currentWeight == 0)
|
||||
throw new Exception("Request limit reached without any prior request. " +
|
||||
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
||||
|
||||
// Wait until the next entry should be removed from the history
|
||||
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
|
||||
if (thisWaitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
historyTopic.Semaphore.Release();
|
||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||
log.Write(LogLevel.Warning, msg);
|
||||
return new CallResult<int>(new RateLimitError(msg));
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
totalWaitTime += thisWaitTime;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var newTime = DateTime.UtcNow;
|
||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||
historyTopic.Semaphore.Release();
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
var newTime = DateTime.UtcNow;
|
||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
finally
|
||||
{
|
||||
historyTopic.Semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
internal struct LimitEntry
|
||||
@@ -330,6 +347,24 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
internal class ConnectionRateLimiter : PartialEndpointRateLimiter
|
||||
{
|
||||
public ConnectionRateLimiter(int limit, TimeSpan perPeriod)
|
||||
: base(new[] { "/" }, limit, perPeriod, null, true, true)
|
||||
{
|
||||
}
|
||||
|
||||
public ConnectionRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod)
|
||||
: base(endpoints, limit, perPeriod, null, true, true)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(ConnectionRateLimiter);
|
||||
}
|
||||
}
|
||||
|
||||
internal class EndpointRateLimiter: Limiter
|
||||
{
|
||||
public string[] Endpoints { get; set; }
|
||||
|
||||
+26
-17
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// An update received from a socket update subscription
|
||||
@@ -12,38 +12,34 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// The timestamp the data was received
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The topic of the update, what symbol/asset etc..
|
||||
/// </summary>
|
||||
public string? Topic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of update
|
||||
/// </summary>
|
||||
public SocketUpdateType? UpdateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The received data deserialized into an object
|
||||
/// </summary>
|
||||
public T Data { get; set; }
|
||||
|
||||
internal DataEvent(T data, DateTime timestamp)
|
||||
{
|
||||
Data = data;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
internal DataEvent(T data, string? topic, DateTime timestamp)
|
||||
{
|
||||
Data = data;
|
||||
Topic = topic;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp)
|
||||
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
||||
{
|
||||
Data = data;
|
||||
Topic = topic;
|
||||
OriginalData = originalData;
|
||||
Timestamp = timestamp;
|
||||
UpdateType = updateType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,7 +50,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data)
|
||||
{
|
||||
return new DataEvent<K>(data, Topic, OriginalData, Timestamp);
|
||||
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -66,7 +62,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string? topic)
|
||||
{
|
||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp);
|
||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The type of the new data</typeparam>
|
||||
/// <param name="data">The new data</param>
|
||||
/// <param name="topic">The new topic</param>
|
||||
/// <param name="updateType">The type of update</param>
|
||||
/// <returns></returns>
|
||||
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType)
|
||||
{
|
||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType);
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-27
@@ -1,24 +1,24 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscription to a data stream
|
||||
/// </summary>
|
||||
public class UpdateSubscription
|
||||
{
|
||||
private readonly SocketConnection connection;
|
||||
private readonly SocketSubscription subscription;
|
||||
private readonly SocketConnection _connection;
|
||||
private readonly Subscription _listener;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
||||
/// </summary>
|
||||
public event Action ConnectionLost
|
||||
{
|
||||
add => connection.ConnectionLost += value;
|
||||
remove => connection.ConnectionLost -= value;
|
||||
add => _connection.ConnectionLost += value;
|
||||
remove => _connection.ConnectionLost -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
add => connection.ConnectionClosed += value;
|
||||
remove => connection.ConnectionClosed -= value;
|
||||
add => _connection.ConnectionClosed += value;
|
||||
remove => _connection.ConnectionClosed -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored
|
||||
{
|
||||
add => connection.ConnectionRestored += value;
|
||||
remove => connection.ConnectionRestored -= value;
|
||||
add => _connection.ConnectionRestored += value;
|
||||
remove => _connection.ConnectionRestored -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,8 +46,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityPaused
|
||||
{
|
||||
add => connection.ActivityPaused += value;
|
||||
remove => connection.ActivityPaused -= value;
|
||||
add => _connection.ActivityPaused += value;
|
||||
remove => _connection.ActivityPaused -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,8 +55,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityUnpaused
|
||||
{
|
||||
add => connection.ActivityUnpaused += value;
|
||||
remove => connection.ActivityUnpaused -= value;
|
||||
add => _connection.ActivityUnpaused += value;
|
||||
remove => _connection.ActivityUnpaused -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,38 +64,38 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public event Action<Exception> Exception
|
||||
{
|
||||
add => subscription.Exception += value;
|
||||
remove => subscription.Exception -= value;
|
||||
add => _listener.Exception += value;
|
||||
remove => _listener.Exception -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The id of the socket
|
||||
/// </summary>
|
||||
public int SocketId => connection.SocketId;
|
||||
public int SocketId => _connection.SocketId;
|
||||
|
||||
/// <summary>
|
||||
/// The id of the subscription
|
||||
/// </summary>
|
||||
public int Id => subscription.Id;
|
||||
public int Id => _listener.Id;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="connection">The socket connection the subscription is on</param>
|
||||
/// <param name="subscription">The subscription</param>
|
||||
public UpdateSubscription(SocketConnection connection, SocketSubscription subscription)
|
||||
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
||||
{
|
||||
this.connection = connection;
|
||||
this.subscription = subscription;
|
||||
_connection = connection;
|
||||
_listener = subscription;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Close the subscription
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task CloseAsync()
|
||||
{
|
||||
return connection.CloseAsync(subscription);
|
||||
return _connection.CloseAsync(_listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,7 +104,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public Task ReconnectAsync()
|
||||
{
|
||||
return connection.TriggerReconnectAsync();
|
||||
return _connection.TriggerReconnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,16 +113,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
internal async Task UnsubscribeAsync()
|
||||
{
|
||||
await connection.UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||
await _connection.UnsubscribeAsync(_listener).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resubscribe this subscription
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal async Task<CallResult<bool>> ResubscribeAsync()
|
||||
internal async Task<CallResult> ResubscribeAsync()
|
||||
{
|
||||
return await connection.ResubscribeAsync(subscription).ConfigureAwait(false);
|
||||
return await _connection.ResubscribeAsync(_listener).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-16
@@ -1,9 +1,9 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for a websocket
|
||||
@@ -14,51 +14,46 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// The uri to connect to
|
||||
/// </summary>
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Headers to send in the connection handshake
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Cookies to send in the connection handshake
|
||||
/// </summary>
|
||||
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Proxy for the connection
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the socket should automatically reconnect when connection is lost
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||
/// </summary>
|
||||
public TimeSpan? Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Interval at which to send ping frames
|
||||
/// </summary>
|
||||
public TimeSpan? KeepAliveInterval { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of messages to send per second
|
||||
/// </summary>
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
/// <summary>
|
||||
/// Origin header value to send in the connection handshake
|
||||
/// </summary>
|
||||
public string? Origin { get; set; }
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||
/// The rate limiters for the socket connection
|
||||
/// </summary>
|
||||
public Func<string, string>? DataInterpreterString { get; set; }
|
||||
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Encoding for sending/receiving data
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
@@ -45,7 +44,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
public Log Log { get; }
|
||||
public ILogger Logger { get; }
|
||||
/// <summary>
|
||||
/// Should synchronize time
|
||||
/// </summary>
|
||||
@@ -62,13 +61,13 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="recalculationInterval"></param>
|
||||
/// <param name="syncTime"></param>
|
||||
/// <param name="syncState"></param>
|
||||
public TimeSyncInfo(Log log, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
|
||||
public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
|
||||
{
|
||||
Log = log;
|
||||
Logger = logger;
|
||||
SyncTime = syncTime;
|
||||
RecalculationInterval = recalculationInterval;
|
||||
TimeSyncState = syncState;
|
||||
@@ -83,12 +82,12 @@ namespace CryptoExchange.Net.Objects
|
||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
||||
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
||||
{
|
||||
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
|
||||
TimeSyncState.TimeOffset = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Write(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
TimeSyncState.TimeOffset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trace logger provider for creating trace loggers
|
||||
/// </summary>
|
||||
public class TraceLoggerProvider : ILoggerProvider
|
||||
{
|
||||
private readonly LogLevel _logLevel;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logLevel"></param>
|
||||
public TraceLoggerProvider(LogLevel? logLevel = null)
|
||||
{
|
||||
_logLevel = logLevel ?? LogLevel.Trace;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trace logger
|
||||
/// </summary>
|
||||
public class TraceLogger : ILogger
|
||||
{
|
||||
private string? _categoryName;
|
||||
private LogLevel _logLevel;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="categoryName"></param>
|
||||
/// <param name="level"></param>
|
||||
public TraceLogger(string? categoryName = null, LogLevel level = LogLevel.Trace)
|
||||
{
|
||||
_categoryName = categoryName;
|
||||
_logLevel = level;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel;
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if ((int)logLevel < (int)_logLevel)
|
||||
return;
|
||||
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment names
|
||||
/// </summary>
|
||||
public static class TradeEnvironmentNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Live environment
|
||||
/// </summary>
|
||||
public const string Live = "live";
|
||||
/// <summary>
|
||||
/// Testnet environment
|
||||
/// </summary>
|
||||
public const string Testnet = "testnet";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the echange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
public class TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the environment
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
protected TradeEnvironment(string name)
|
||||
{
|
||||
EnvironmentName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,17 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// First sequence number in this update
|
||||
/// </summary>
|
||||
public long FirstUpdateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last sequence number in this update
|
||||
/// </summary>
|
||||
public long LastUpdateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of changed/new asks
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||
|
||||
/// <summary>
|
||||
/// List of changed/new bids
|
||||
/// </summary>
|
||||
|
||||
@@ -7,9 +7,9 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.OrderBook
|
||||
@@ -30,7 +30,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
private readonly AsyncResetEvent _queueEvent;
|
||||
private readonly ConcurrentQueue<object> _processQueue;
|
||||
private readonly bool _validateChecksum;
|
||||
private bool _validateChecksum;
|
||||
|
||||
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
|
||||
{
|
||||
@@ -40,48 +40,47 @@ namespace CryptoExchange.Net.OrderBook
|
||||
set { } }
|
||||
}
|
||||
|
||||
private static readonly ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
||||
|
||||
private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
|
||||
|
||||
/// <summary>
|
||||
/// A buffer to store messages received before the initial book snapshot is processed. These messages
|
||||
/// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower
|
||||
/// than the snapshot sequence number will be discarded
|
||||
/// </summary>
|
||||
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
|
||||
protected readonly List<ProcessBufferRangeSequenceEntry> _processBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// The ask list, should only be accessed using the bookLock
|
||||
/// </summary>
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> _asks;
|
||||
|
||||
/// <summary>
|
||||
/// The bid list, should only be accessed using the bookLock
|
||||
/// </summary>
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
|
||||
protected SortedList<decimal, ISymbolOrderBookEntry> _bids;
|
||||
|
||||
/// <summary>
|
||||
/// The log
|
||||
/// </summary>
|
||||
protected Log log;
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1
|
||||
/// the book will resynchronize as it is deemed out of sync
|
||||
/// </summary>
|
||||
protected bool sequencesAreConsecutive;
|
||||
protected bool _sequencesAreConsecutive;
|
||||
|
||||
/// <summary>
|
||||
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
|
||||
/// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the
|
||||
/// server handle this
|
||||
/// </summary>
|
||||
protected bool strictLevels;
|
||||
protected bool _strictLevels;
|
||||
|
||||
/// <summary>
|
||||
/// If the initial snapshot of the book has been set
|
||||
/// </summary>
|
||||
protected bool bookSet;
|
||||
protected bool _bookSet;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of levels for this book
|
||||
@@ -102,7 +101,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
var old = _status;
|
||||
_status = value;
|
||||
log.Write(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
|
||||
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
|
||||
OnStatusChange?.Invoke(old, _status);
|
||||
}
|
||||
}
|
||||
@@ -137,7 +136,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return asks.Select(a => a.Value).ToList();
|
||||
return _asks.Select(a => a.Value).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +146,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return bids.Select(a => a.Value).ToList();
|
||||
return _bids.Select(a => a.Value).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +166,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
|
||||
return _bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
get
|
||||
{
|
||||
lock (_bookLock)
|
||||
return asks.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
|
||||
return _asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,32 +191,39 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger to use. If not provided will create a TraceLogger</param>
|
||||
/// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</param>
|
||||
/// <param name="symbol">The symbol the order book is for</param>
|
||||
/// <param name="options">The options for the order book</param>
|
||||
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
|
||||
protected SymbolOrderBook(ILogger? logger, string id, string symbol)
|
||||
{
|
||||
if (symbol == null)
|
||||
throw new ArgumentNullException(nameof(symbol));
|
||||
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
Id = id;
|
||||
processBuffer = new List<ProcessBufferRangeSequenceEntry>();
|
||||
_processBuffer = new List<ProcessBufferRangeSequenceEntry>();
|
||||
_processQueue = new ConcurrentQueue<object>();
|
||||
_queueEvent = new AsyncResetEvent(false, true);
|
||||
|
||||
_validateChecksum = options.ChecksumValidationEnabled;
|
||||
Symbol = symbol;
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
|
||||
asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
||||
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
||||
_asks = new SortedList<decimal, ISymbolOrderBookEntry>();
|
||||
_bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
|
||||
|
||||
log = new Log(id) { Level = options.LogLevel };
|
||||
var writers = options.LogWriters ?? new List<ILogger> { new DebugLogger() };
|
||||
log.UpdateWriters(writers.ToList());
|
||||
_logger = logger ?? new TraceLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the order book using the provided options
|
||||
/// </summary>
|
||||
/// <param name="options">The options</param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
protected void Initialize(OrderBookOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
_validateChecksum = options.ChecksumValidationEnabled;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -226,7 +232,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
if (Status != OrderBookStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
|
||||
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} starting");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} starting");
|
||||
_cts = new CancellationTokenSource();
|
||||
ct?.Register(async () =>
|
||||
{
|
||||
@@ -236,8 +242,8 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
// Clear any previous messages
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
processBuffer.Clear();
|
||||
bookSet = false;
|
||||
_processBuffer.Clear();
|
||||
_bookSet = false;
|
||||
|
||||
Status = OrderBookStatus.Connecting;
|
||||
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
|
||||
@@ -251,7 +257,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (_cts.IsCancellationRequested)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
|
||||
await startResult.Data.CloseAsync().ConfigureAwait(false);
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
@@ -267,7 +273,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
|
||||
private void HandleConnectionLost() {
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
|
||||
if (Status != OrderBookStatus.Disposed) {
|
||||
Status = OrderBookStatus.Reconnecting;
|
||||
Reset();
|
||||
@@ -275,7 +281,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
}
|
||||
|
||||
private void HandleConnectionClosed() {
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
}
|
||||
@@ -287,7 +293,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// <inheritdoc/>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
|
||||
Status = OrderBookStatus.Disconnected;
|
||||
_cts?.Cancel();
|
||||
_queueEvent.Set();
|
||||
@@ -300,21 +306,21 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_subscription.ConnectionClosed -= HandleConnectionClosed;
|
||||
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||
}
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type)
|
||||
public CallResult<decimal> CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type)
|
||||
{
|
||||
if (Status != OrderBookStatus.Synced)
|
||||
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
|
||||
|
||||
var totalCost = 0m;
|
||||
var totalAmount = 0m;
|
||||
var amountLeft = quantity;
|
||||
var amountLeft = baseQuantity;
|
||||
lock (_bookLock)
|
||||
{
|
||||
var list = type == OrderBookEntryType.Ask ? asks : bids;
|
||||
var list = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
|
||||
var step = 0;
|
||||
while (amountLeft > 0)
|
||||
@@ -334,6 +340,35 @@ namespace CryptoExchange.Net.OrderBook
|
||||
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type)
|
||||
{
|
||||
if (Status != OrderBookStatus.Synced)
|
||||
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
|
||||
|
||||
var quoteQuantityLeft = quoteQuantity;
|
||||
var totalBaseQuantity = 0m;
|
||||
lock (_bookLock)
|
||||
{
|
||||
var list = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
|
||||
var step = 0;
|
||||
while (quoteQuantityLeft > 0)
|
||||
{
|
||||
if (step == list.Count)
|
||||
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
|
||||
|
||||
var element = list.ElementAt(step);
|
||||
var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft);
|
||||
quoteQuantityLeft -= stepAmount;
|
||||
totalBaseQuantity += stepAmount / element.Value.Price;
|
||||
step++;
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<decimal>(Math.Round(totalBaseQuantity, 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving
|
||||
/// and setting the initial order book
|
||||
@@ -427,14 +462,14 @@ namespace CryptoExchange.Net.OrderBook
|
||||
/// </summary>
|
||||
protected void CheckProcessBuffer()
|
||||
{
|
||||
var pbList = processBuffer.ToList();
|
||||
var pbList = _processBuffer.ToList();
|
||||
if (pbList.Count > 0)
|
||||
log.Write(LogLevel.Debug, $"Processing {pbList.Count} buffered updates");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} Processing {pbList.Count} buffered updates");
|
||||
|
||||
foreach (var bufferEntry in pbList)
|
||||
{
|
||||
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
|
||||
processBuffer.Remove(bufferEntry);
|
||||
_processBuffer.Remove(bufferEntry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,21 +483,21 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (sequence <= LastSequenceNumber)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}, currently at #{LastSequenceNumber}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
|
||||
if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
|
||||
{
|
||||
// Out of sync
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
|
||||
_stopProcessing = true;
|
||||
Resubscribe();
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTime = DateTime.UtcNow;
|
||||
var listToChange = type == OrderBookEntryType.Ask ? asks : bids;
|
||||
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
|
||||
if (entry.Quantity == 0)
|
||||
{
|
||||
if (!listToChange.ContainsKey(entry.Price))
|
||||
@@ -498,7 +533,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
|
||||
{
|
||||
var startWait = DateTime.UtcNow;
|
||||
while (!bookSet && Status == OrderBookStatus.Syncing)
|
||||
while (!_bookSet && Status == OrderBookStatus.Syncing)
|
||||
{
|
||||
if(ct.IsCancellationRequested)
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
@@ -540,9 +575,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
// Clear queue
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
|
||||
processBuffer.Clear();
|
||||
asks.Clear();
|
||||
bids.Clear();
|
||||
_processBuffer.Clear();
|
||||
_asks.Clear();
|
||||
_bids.Clear();
|
||||
AskCount = 0;
|
||||
BidCount = 0;
|
||||
|
||||
@@ -581,7 +616,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var (bestBid, bestAsk) = BestOffers;
|
||||
if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity ||
|
||||
bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity)
|
||||
{
|
||||
OnBestOffersChanged?.Invoke((bestBid, bestAsk));
|
||||
}
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
@@ -589,8 +626,8 @@ namespace CryptoExchange.Net.OrderBook
|
||||
_queueEvent.Set();
|
||||
// Clear queue
|
||||
while (_processQueue.TryDequeue(out _)) { }
|
||||
processBuffer.Clear();
|
||||
bookSet = false;
|
||||
_processBuffer.Clear();
|
||||
_bookSet = false;
|
||||
DoReset();
|
||||
}
|
||||
|
||||
@@ -607,7 +644,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
success = resyncResult;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
|
||||
_logger.Log(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
|
||||
Status = OrderBookStatus.Synced;
|
||||
}
|
||||
|
||||
@@ -624,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (_stopProcessing)
|
||||
{
|
||||
log.Write(LogLevel.Trace, "Skipping message because of resubscribing");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -642,21 +679,21 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
lock (_bookLock)
|
||||
{
|
||||
bookSet = true;
|
||||
asks.Clear();
|
||||
_bookSet = true;
|
||||
_asks.Clear();
|
||||
foreach (var ask in item.Asks)
|
||||
asks.Add(ask.Price, ask);
|
||||
bids.Clear();
|
||||
_asks.Add(ask.Price, ask);
|
||||
_bids.Clear();
|
||||
foreach (var bid in item.Bids)
|
||||
bids.Add(bid.Price, bid);
|
||||
_bids.Add(bid.Price, bid);
|
||||
|
||||
LastSequenceNumber = item.EndUpdateId;
|
||||
|
||||
AskCount = asks.Count;
|
||||
BidCount = bids.Count;
|
||||
AskCount = _asks.Count;
|
||||
BidCount = _bids.Count;
|
||||
|
||||
UpdateTime = DateTime.UtcNow;
|
||||
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
|
||||
_logger.Log(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
|
||||
CheckProcessBuffer();
|
||||
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
|
||||
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
||||
@@ -667,16 +704,16 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
lock (_bookLock)
|
||||
{
|
||||
if (!bookSet)
|
||||
if (!_bookSet)
|
||||
{
|
||||
processBuffer.Add(new ProcessBufferRangeSequenceEntry()
|
||||
_processBuffer.Add(new ProcessBufferRangeSequenceEntry()
|
||||
{
|
||||
Asks = item.Asks,
|
||||
Bids = item.Bids,
|
||||
FirstUpdateId = item.StartUpdateId,
|
||||
LastUpdateId = item.EndUpdateId,
|
||||
});
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -684,12 +721,12 @@ namespace CryptoExchange.Net.OrderBook
|
||||
var (prevBestBid, prevBestAsk) = BestOffers;
|
||||
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
|
||||
|
||||
if (!asks.Any() || !bids.Any())
|
||||
if (!_asks.Any() || !_bids.Any())
|
||||
return;
|
||||
|
||||
if (asks.First().Key < bids.First().Key)
|
||||
if (_asks.First().Key < _bids.First().Key)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {asks.First().Key}, first bid: {bids.First().Key}. Resyncing");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {_asks.First().Key}, first bid: {_bids.First().Key}. Resyncing");
|
||||
_stopProcessing = true;
|
||||
Resubscribe();
|
||||
return;
|
||||
@@ -723,7 +760,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
|
||||
if (!checksumResult)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
|
||||
_stopProcessing = true;
|
||||
Resubscribe();
|
||||
}
|
||||
@@ -747,12 +784,14 @@ namespace CryptoExchange.Net.OrderBook
|
||||
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
|
||||
{
|
||||
// Resubscribing failed, reconnect the socket
|
||||
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
|
||||
_logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
|
||||
Status = OrderBookStatus.Reconnecting;
|
||||
_ = _subscription!.ReconnectAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ResyncAsync().ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -760,7 +799,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
if (lastUpdateId <= LastSequenceNumber)
|
||||
{
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -770,23 +809,23 @@ namespace CryptoExchange.Net.OrderBook
|
||||
foreach (var entry in asks)
|
||||
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry);
|
||||
|
||||
if (Levels.HasValue && strictLevels)
|
||||
if (Levels.HasValue && _strictLevels)
|
||||
{
|
||||
while (this.bids.Count > Levels.Value)
|
||||
while (this._bids.Count > Levels.Value)
|
||||
{
|
||||
BidCount--;
|
||||
this.bids.Remove(this.bids.Last().Key);
|
||||
this._bids.Remove(this._bids.Last().Key);
|
||||
}
|
||||
|
||||
while (this.asks.Count > Levels.Value)
|
||||
while (this._asks.Count > Levels.Value)
|
||||
{
|
||||
AskCount--;
|
||||
this.asks.Remove(this.asks.Last().Key);
|
||||
this._asks.Remove(this._asks.Last().Key);
|
||||
}
|
||||
}
|
||||
|
||||
LastSequenceNumber = lastUpdateId;
|
||||
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
|
||||
_logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
public class Request : IRequest
|
||||
{
|
||||
private readonly HttpRequestMessage request;
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly HttpRequestMessage _request;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Create request object for web request
|
||||
@@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <param name="requestId"></param>
|
||||
public Request(HttpRequestMessage request, HttpClient client, int requestId)
|
||||
{
|
||||
httpClient = client;
|
||||
this.request = request;
|
||||
_httpClient = client;
|
||||
_request = request;
|
||||
RequestId = requestId;
|
||||
}
|
||||
|
||||
@@ -37,18 +37,18 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <inheritdoc />
|
||||
public string Accept
|
||||
{
|
||||
set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
|
||||
set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpMethod Method
|
||||
{
|
||||
get => request.Method;
|
||||
set => request.Method = value;
|
||||
get => _request.Method;
|
||||
set => _request.Method = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => request.RequestUri;
|
||||
public Uri Uri => _request.RequestUri;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int RequestId { get; }
|
||||
@@ -57,31 +57,31 @@ namespace CryptoExchange.Net.Requests
|
||||
public void SetContent(string data, string contentType)
|
||||
{
|
||||
Content = data;
|
||||
request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
_request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddHeader(string key, string value)
|
||||
{
|
||||
request.Headers.Add(key, value);
|
||||
_request.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, IEnumerable<string>> GetHeaders()
|
||||
{
|
||||
return request.Headers.ToDictionary(h => h.Key, h => h.Value);
|
||||
return _request.Headers.ToDictionary(h => h.Key, h => h.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContent(byte[] data)
|
||||
{
|
||||
request.Content = new ByteArrayContent(data);
|
||||
_request.Content = new ByteArrayContent(data);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
||||
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,37 +11,39 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
private HttpClient? httpClient;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? client = null)
|
||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
HttpMessageHandler handler = new HttpClientHandler()
|
||||
var handler = new HttpClientHandler();
|
||||
if (proxy != null)
|
||||
{
|
||||
Proxy = proxy == null ? null : new WebProxy
|
||||
handler.Proxy = new WebProxy
|
||||
{
|
||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
httpClient = new HttpClient(handler) { Timeout = requestTimeout };
|
||||
}
|
||||
else
|
||||
{
|
||||
httpClient = client;
|
||||
client = new HttpClient(handler)
|
||||
{
|
||||
Timeout = requestTimeout
|
||||
};
|
||||
}
|
||||
|
||||
_httpClient = client;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||
{
|
||||
if (httpClient == null)
|
||||
if (_httpClient == null)
|
||||
throw new InvalidOperationException("Cant create request before configuring http client");
|
||||
|
||||
return new Request(new HttpRequestMessage(method, uri), httpClient, requestId);
|
||||
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
internal class Response : IResponse
|
||||
{
|
||||
private readonly HttpResponseMessage response;
|
||||
private readonly HttpResponseMessage _response;
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpStatusCode StatusCode => response.StatusCode;
|
||||
public HttpStatusCode StatusCode => _response.StatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSuccessStatusCode => response.IsSuccessStatusCode;
|
||||
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => response.Headers;
|
||||
public long? ContentLength => _response.Content.Headers.ContentLength;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => _response.Headers;
|
||||
|
||||
/// <summary>
|
||||
/// Create response for a http response message
|
||||
@@ -29,19 +32,19 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <param name="response">The actual response</param>
|
||||
public Response(HttpResponseMessage response)
|
||||
{
|
||||
this.response = response;
|
||||
this._response = response;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Stream> GetResponseStreamAsync()
|
||||
{
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await _response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Close()
|
||||
{
|
||||
response.Dispose();
|
||||
_response.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -8,6 +8,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -27,14 +28,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
Reconnecting
|
||||
}
|
||||
|
||||
internal static int lastStreamId;
|
||||
private static readonly object streamIdLock = new();
|
||||
internal static int _lastStreamId;
|
||||
private static readonly object _streamIdLock = new();
|
||||
|
||||
private readonly AsyncResetEvent _sendEvent;
|
||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
||||
private readonly ConcurrentQueue<SendItem> _sendBuffer;
|
||||
private readonly SemaphoreSlim _closeSem;
|
||||
private readonly WebSocketParameters _parameters;
|
||||
private readonly List<DateTime> _outgoingMessages;
|
||||
|
||||
private ClientWebSocket _socket;
|
||||
private CancellationTokenSource _ctsSource;
|
||||
@@ -44,7 +43,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
private bool _stopRequested;
|
||||
private bool _disposed;
|
||||
private ProcessState _processState;
|
||||
|
||||
private DateTime _lastReconnectTime;
|
||||
|
||||
/// <summary>
|
||||
/// Received messages, the size and the timstamp
|
||||
@@ -59,24 +58,27 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Log
|
||||
/// </summary>
|
||||
protected Log _log;
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public WebSocketParameters Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp this socket has been active for the last time
|
||||
/// </summary>
|
||||
public DateTime LastActionTime { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => _parameters.Uri;
|
||||
public Uri Uri => Parameters.Uri;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||
public virtual bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
|
||||
public virtual bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
@@ -96,33 +98,42 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnClose;
|
||||
public event Func<Task>? OnClose;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<string>? OnMessage;
|
||||
public event Func<WebSocketMessageType, Stream, Task>? OnStreamMessage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<Exception>? OnError;
|
||||
public event Func<int, Task>? OnRequestSent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnOpen;
|
||||
public event Func<Exception, Task>? OnError;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnReconnecting;
|
||||
public event Func<Task>? OnOpen;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnReconnected;
|
||||
public event Func<Task>? OnReconnecting;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<Task>? OnReconnected;
|
||||
/// <inheritdoc />
|
||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log">The log object to use</param>
|
||||
/// <param name="logger">The log object to use</param>
|
||||
/// <param name="websocketParameters">The parameters for this socket</param>
|
||||
public CryptoExchangeWebSocketClient(Log log, WebSocketParameters websocketParameters)
|
||||
public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters)
|
||||
{
|
||||
Id = NextStreamId();
|
||||
_log = log;
|
||||
_logger = logger;
|
||||
|
||||
_parameters = websocketParameters;
|
||||
_outgoingMessages = new List<DateTime>();
|
||||
Parameters = websocketParameters;
|
||||
_receivedMessages = new List<ReceiveItem>();
|
||||
_sendEvent = new AsyncResetEvent();
|
||||
_sendBuffer = new ConcurrentQueue<byte[]>();
|
||||
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||
_ctsSource = new CancellationTokenSource();
|
||||
_receivedMessagesLock = new object();
|
||||
|
||||
@@ -136,7 +147,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!await ConnectInternalAsync().ConfigureAwait(false))
|
||||
return false;
|
||||
|
||||
OnOpen?.Invoke();
|
||||
await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_processTask = ProcessAsync();
|
||||
return true;
|
||||
}
|
||||
@@ -147,23 +158,32 @@ namespace CryptoExchange.Net.Sockets
|
||||
private ClientWebSocket CreateSocket()
|
||||
{
|
||||
var cookieContainer = new CookieContainer();
|
||||
foreach (var cookie in _parameters.Cookies)
|
||||
foreach (var cookie in Parameters.Cookies)
|
||||
cookieContainer.Add(new Cookie(cookie.Key, cookie.Value));
|
||||
|
||||
var socket = new ClientWebSocket();
|
||||
socket.Options.Cookies = cookieContainer;
|
||||
foreach (var header in _parameters.Headers)
|
||||
socket.Options.SetRequestHeader(header.Key, header.Value);
|
||||
socket.Options.KeepAliveInterval = _parameters.KeepAliveInterval ?? TimeSpan.Zero;
|
||||
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
|
||||
if (_parameters.Proxy != null)
|
||||
SetProxy(_parameters.Proxy);
|
||||
try
|
||||
{
|
||||
socket.Options.Cookies = cookieContainer;
|
||||
foreach (var header in Parameters.Headers)
|
||||
socket.Options.SetRequestHeader(header.Key, header.Value);
|
||||
socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero;
|
||||
socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
|
||||
if (Parameters.Proxy != null)
|
||||
SetProxy(socket, Parameters.Proxy);
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
// Options are not supported on certain platforms (WebAssembly for instance)
|
||||
// best we can do it try to connect without setting options.
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
private async Task<bool> ConnectInternalAsync()
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connecting");
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
|
||||
@@ -171,11 +191,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connection failed: " + e.ToLogString());
|
||||
return false;
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connected to {Uri}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -184,13 +204,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
while (!_stopRequested)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting processing tasks");
|
||||
_processState = ProcessState.Processing;
|
||||
var sendTask = SendLoopAsync();
|
||||
var receiveTask = ReceiveLoopAsync();
|
||||
var timeoutTask = _parameters.Timeout != null && _parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} processing tasks finished");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] processing tasks finished");
|
||||
|
||||
_processState = ProcessState.WaitingForClose;
|
||||
while (_closeTask == null)
|
||||
@@ -199,22 +219,37 @@ namespace CryptoExchange.Net.Sockets
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
_closeTask = null;
|
||||
|
||||
if (!_parameters.AutoReconnect)
|
||||
if (!Parameters.AutoReconnect)
|
||||
{
|
||||
_processState = ProcessState.Idle;
|
||||
OnClose?.Invoke();
|
||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_stopRequested)
|
||||
{
|
||||
_processState = ProcessState.Reconnecting;
|
||||
OnReconnecting?.Invoke();
|
||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
||||
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
||||
|
||||
while (!_stopRequested)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] attempting to reconnect");
|
||||
var task = GetReconnectionUrl?.Invoke();
|
||||
if (task != null)
|
||||
{
|
||||
var reconnectUri = await task.ConfigureAwait(false);
|
||||
if (reconnectUri != null && Parameters.Uri != reconnectUri)
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect URI set to {reconnectUri}");
|
||||
Parameters.Uri = reconnectUri;
|
||||
}
|
||||
}
|
||||
|
||||
_socket = CreateSocket();
|
||||
_ctsSource.Dispose();
|
||||
_ctsSource = new CancellationTokenSource();
|
||||
@@ -223,11 +258,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||
if (!connected)
|
||||
{
|
||||
await Task.Delay(_parameters.ReconnectInterval).ConfigureAwait(false);
|
||||
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
OnReconnected?.Invoke();
|
||||
_lastReconnectTime = DateTime.UtcNow;
|
||||
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -236,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Send(string data)
|
||||
public virtual void Send(int id, string data, int weight)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var bytes = _parameters.Encoding.GetBytes(data);
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
||||
_sendBuffer.Enqueue(bytes);
|
||||
var bytes = Parameters.Encoding.GetBytes(data);
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {id} - Adding {bytes.Length} bytes to send buffer");
|
||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||
_sendEvent.Set();
|
||||
}
|
||||
|
||||
@@ -253,7 +289,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_processState != ProcessState.Processing && IsOpen)
|
||||
return;
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect requested");
|
||||
_closeTask = CloseInternalAsync();
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
}
|
||||
@@ -262,24 +298,24 @@ namespace CryptoExchange.Net.Sockets
|
||||
public virtual async Task CloseAsync()
|
||||
{
|
||||
await _closeSem.WaitAsync().ConfigureAwait(false);
|
||||
_stopRequested = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_closeTask != null && !_closeTask.IsCompleted)
|
||||
if (_closeTask?.IsCompleted == false)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() waiting for existing close task");
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_stopRequested = true;
|
||||
|
||||
if (!IsOpen)
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() socket not open");
|
||||
return;
|
||||
}
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closing");
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
finally
|
||||
@@ -290,8 +326,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
if(_processTask != null)
|
||||
await _processTask.ConfigureAwait(false);
|
||||
OnClose?.Invoke();
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} closed");
|
||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -343,11 +379,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} disposing");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] disposing");
|
||||
_disposed = true;
|
||||
_socket.Dispose();
|
||||
_ctsSource.Dispose();
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] disposed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -358,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
try
|
||||
{
|
||||
var limitKey = Uri.ToString() + "/" + Id.ToString();
|
||||
while (true)
|
||||
{
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
@@ -370,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
while (_sendBuffer.TryDequeue(out var data))
|
||||
{
|
||||
if (_parameters.RatelimitPerSecond != null)
|
||||
if (Parameters.RateLimiters != null)
|
||||
{
|
||||
// Wait for rate limit
|
||||
DateTime? start = null;
|
||||
while (MessagesSentLastSecond() >= _parameters.RatelimitPerSecond)
|
||||
foreach(var ratelimiter in Parameters.RateLimiters)
|
||||
{
|
||||
start ??= DateTime.UtcNow;
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
|
||||
if (limitResult.Success)
|
||||
{
|
||||
if (limitResult.Data > 0)
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
|
||||
}
|
||||
}
|
||||
|
||||
if (start != null)
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
_outgoingMessages.Add(DateTime.UtcNow);
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {data.Id} - sent {data.Bytes.Length} bytes");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -398,8 +434,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
catch (Exception ioe)
|
||||
{
|
||||
// Connection closed unexpectedly, .NET framework
|
||||
OnError?.Invoke(ioe);
|
||||
_closeTask = CloseInternalAsync();
|
||||
await (OnError?.Invoke(ioe) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -410,12 +447,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
|
||||
// Make sure we at least let the owner know there was an error
|
||||
OnError?.Invoke(e);
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] send loop stopped with exception");
|
||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} Send loop finished");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] send loop finished");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,16 +492,18 @@ namespace CryptoExchange.Net.Sockets
|
||||
catch (Exception wse)
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
OnError?.Invoke(wse);
|
||||
_closeTask = CloseInternalAsync();
|
||||
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
// Connection closed unexpectedly
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
|
||||
_closeTask = CloseInternalAsync();
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] received `Close` message");
|
||||
if (_closeTask?.IsCompleted != false)
|
||||
_closeTask = CloseInternalAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -472,7 +512,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// We received data, but it is not complete, write it to a memory stream for reassembling
|
||||
multiPartMessage = true;
|
||||
memoryStream ??= new MemoryStream();
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
|
||||
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -480,13 +520,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!multiPartMessage)
|
||||
{
|
||||
// Received a complete message and it's not multi part
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
||||
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in single message");
|
||||
await ProcessData(receiveResult.MessageType, new MemoryStream(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Received the end of a multipart message, write to memory stream for reassembling
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
|
||||
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
@@ -514,12 +554,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (receiveResult?.EndOfMessage == true)
|
||||
{
|
||||
// Reassemble complete message from memory stream
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
|
||||
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] reassembled message of {memoryStream!.Length} bytes");
|
||||
await ProcessData(receiveResult.MessageType, memoryStream).ConfigureAwait(false);
|
||||
memoryStream.Dispose();
|
||||
}
|
||||
else
|
||||
_log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
|
||||
{
|
||||
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] discarding incomplete message of {memoryStream!.Length} bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -528,112 +570,38 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
|
||||
// Make sure we at least let the owner know there was an error
|
||||
OnError?.Invoke(e);
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] receive loop stopped with exception");
|
||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} Receive loop finished");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] receive loop finished");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the message
|
||||
/// Proccess a stream message
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="messageType"></param>
|
||||
private void HandleMessage(byte[] data, int offset, int count, WebSocketMessageType messageType)
|
||||
{
|
||||
string strData;
|
||||
if (messageType == WebSocketMessageType.Binary)
|
||||
{
|
||||
if (_parameters.DataInterpreterBytes == null)
|
||||
throw new Exception("Byte interpreter not set while receiving byte data");
|
||||
|
||||
try
|
||||
{
|
||||
var relevantData = new byte[count];
|
||||
Array.Copy(data, offset, relevantData, 0, count);
|
||||
strData = _parameters.DataInterpreterBytes(relevantData);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
strData = _parameters.Encoding.GetString(data, offset, count);
|
||||
|
||||
if (_parameters.DataInterpreterString != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
strData = _parameters.DataInterpreterString(strData);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
OnMessage?.Invoke(strData);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the OnMessage event
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
protected void TriggerOnMessage(string data)
|
||||
/// <param name="type"></param>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task ProcessData(WebSocketMessageType type, Stream stream)
|
||||
{
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
OnMessage?.Invoke(data);
|
||||
stream.Position = 0;
|
||||
|
||||
if (OnStreamMessage != null)
|
||||
await OnStreamMessage.Invoke(type, stream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the OnError event
|
||||
/// </summary>
|
||||
/// <param name="ex"></param>
|
||||
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the OnError event
|
||||
/// </summary>
|
||||
protected void TriggerOnOpen() => OnOpen?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the OnError event
|
||||
/// </summary>
|
||||
protected void TriggerOnClose() => OnClose?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the OnReconnecting event
|
||||
/// </summary>
|
||||
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the OnReconnected event
|
||||
/// </summary>
|
||||
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there is no data received for a period longer than the specified timeout
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected async Task CheckTimeoutAsync()
|
||||
{
|
||||
_log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {_parameters.Timeout}");
|
||||
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting task checking for no data received for {Parameters.Timeout}");
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
@@ -642,10 +610,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (DateTime.UtcNow - LastActionTime > _parameters.Timeout)
|
||||
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
|
||||
{
|
||||
_log.Write(LogLevel.Warning, $"Socket {Id} No data received for {_parameters.Timeout}, reconnecting socket");
|
||||
_ = CloseAsync().ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] no data received for {Parameters.Timeout}, reconnecting socket");
|
||||
_ = ReconnectAsync().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
try
|
||||
@@ -664,7 +632,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
// Because this is running in a separate task and not awaited until the socket gets closed
|
||||
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
||||
// Make sure we at least let the owner know there was an error
|
||||
OnError?.Invoke(e);
|
||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -675,20 +643,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
private static int NextStreamId()
|
||||
{
|
||||
lock (streamIdLock)
|
||||
lock (_streamIdLock)
|
||||
{
|
||||
lastStreamId++;
|
||||
return lastStreamId;
|
||||
_lastStreamId++;
|
||||
return _lastStreamId;
|
||||
}
|
||||
}
|
||||
|
||||
private int MessagesSentLastSecond()
|
||||
{
|
||||
var testTime = DateTime.UtcNow;
|
||||
_outgoingMessages.RemoveAll(r => testTime - r > TimeSpan.FromSeconds(1));
|
||||
return _outgoingMessages.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the received messages list, removing messages received longer than 3s ago
|
||||
/// </summary>
|
||||
@@ -698,8 +659,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
|
||||
{
|
||||
foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list
|
||||
{
|
||||
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
|
||||
_receivedMessages.Remove(msg);
|
||||
}
|
||||
|
||||
_lastReceivedMessagesUpdate = checkTime;
|
||||
}
|
||||
@@ -708,25 +671,52 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Set proxy on socket
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="proxy"></param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
protected virtual void SetProxy(ApiProxy proxy)
|
||||
protected virtual void SetProxy(ClientWebSocket socket, ApiProxy proxy)
|
||||
{
|
||||
if (!Uri.TryCreate($"{proxy.Host}:{proxy.Port}", UriKind.Absolute, out var uri))
|
||||
throw new ArgumentException("Proxy settings invalid, {proxy.Host}:{proxy.Port} not a valid URI", nameof(proxy));
|
||||
|
||||
_socket.Options.Proxy = uri?.Scheme == null
|
||||
? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
||||
: _socket.Options.Proxy = new WebProxy
|
||||
socket.Options.Proxy = uri?.Scheme == null
|
||||
? socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
|
||||
: socket.Options.Proxy = new WebProxy
|
||||
{
|
||||
Address = uri
|
||||
};
|
||||
|
||||
if (proxy.Login != null)
|
||||
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message info
|
||||
/// </summary>
|
||||
public struct SendItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
public int Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp the request was sent
|
||||
/// </summary>
|
||||
public DateTime SendTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The bytes to send
|
||||
/// </summary>
|
||||
public byte[] Bytes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Received message info
|
||||
/// </summary>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Message received event
|
||||
/// </summary>
|
||||
public class MessageEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection the message was received on
|
||||
/// </summary>
|
||||
public SocketConnection Connection { get; set; }
|
||||
/// <summary>
|
||||
/// The json object of the data
|
||||
/// </summary>
|
||||
public JToken JsonData { get; set; }
|
||||
/// <summary>
|
||||
/// The originally received string data
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
/// <summary>
|
||||
/// The timestamp of when the data was received
|
||||
/// </summary>
|
||||
public DateTime ReceivedTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="jsonData"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="timestamp"></param>
|
||||
public MessageEvent(SocketConnection connection, JToken jsonData, string? originalData, DateTime timestamp)
|
||||
{
|
||||
Connection = connection;
|
||||
JsonData = jsonData;
|
||||
OriginalData = originalData;
|
||||
ReceivedTimestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Message accessor
|
||||
/// </summary>
|
||||
public interface IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this a json message
|
||||
/// </summary>
|
||||
bool IsJson { get; }
|
||||
/// <summary>
|
||||
/// The underlying data object
|
||||
/// </summary>
|
||||
object? Underlying { get; }
|
||||
/// <summary>
|
||||
/// Load a stream message
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
void Load(Stream stream);
|
||||
/// <summary>
|
||||
/// Get the type of node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
NodeType? GetNodeType();
|
||||
/// <summary>
|
||||
/// Get the type of node
|
||||
/// </summary>
|
||||
/// <param name="path">Access path</param>
|
||||
/// <returns></returns>
|
||||
NodeType? GetNodeType(MessagePath path);
|
||||
/// <summary>
|
||||
/// Get the value of a path
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
T? GetValue<T>(MessagePath path);
|
||||
/// <summary>
|
||||
/// Get the values of an array
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
List<T?>? GetValues<T>(MessagePath path);
|
||||
/// <summary>
|
||||
/// Deserialize the message into this type
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
object Deserialize(Type type, MessagePath? path = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer interface
|
||||
/// </summary>
|
||||
public interface IMessageSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize an object to a string
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
string Serialize(object message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Json.Net message accessor
|
||||
/// </summary>
|
||||
public class JsonNetMessageAccessor : IMessageAccessor
|
||||
{
|
||||
private JToken? _token;
|
||||
private Stream? _stream;
|
||||
private static JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsJson { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => _token;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load(Stream stream)
|
||||
{
|
||||
_stream = stream;
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)stream.Length, true);
|
||||
using var jsonTextReader = new JsonTextReader(reader);
|
||||
|
||||
try
|
||||
{
|
||||
_token = JToken.Load(jsonTextReader);
|
||||
IsJson = true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
if (!IsJson)
|
||||
{
|
||||
var sr = new StreamReader(_stream);
|
||||
return sr.ReadToEnd();
|
||||
}
|
||||
|
||||
var source = _token;
|
||||
if (path != null)
|
||||
source = GetPathNode(path.Value);
|
||||
|
||||
return source!.ToObject(type, _serializer)!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
if (_token == null)
|
||||
return null;
|
||||
|
||||
if (_token.Type == JTokenType.Object)
|
||||
return NodeType.Object;
|
||||
|
||||
if (_token.Type == JTokenType.Array)
|
||||
return NodeType.Array;
|
||||
|
||||
return NodeType.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
var node = GetPathNode(path);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
if (node.Type == JTokenType.Object)
|
||||
return NodeType.Object;
|
||||
|
||||
if (node.Type == JTokenType.Array)
|
||||
return NodeType.Array;
|
||||
|
||||
return NodeType.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
|
||||
return default;
|
||||
|
||||
return value!.Value<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<T?>? GetValues<T>(MessagePath path)
|
||||
{
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Type == JTokenType.Object)
|
||||
return default;
|
||||
|
||||
return value!.Values<T>().ToList();
|
||||
}
|
||||
|
||||
private JToken? GetPathNode(MessagePath path)
|
||||
{
|
||||
var currentToken = _token;
|
||||
foreach (var node in path)
|
||||
{
|
||||
if (node.Type == 0)
|
||||
{
|
||||
// Int value
|
||||
var val = (int)node.Value!;
|
||||
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
|
||||
return null;
|
||||
|
||||
currentToken = currentToken[val];
|
||||
}
|
||||
else if (node.Type == 1)
|
||||
{
|
||||
// String value
|
||||
if (currentToken!.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
currentToken = currentToken[(string)node.Value!];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Property name
|
||||
if (currentToken!.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
currentToken = (currentToken.First as JProperty)?.Name;
|
||||
}
|
||||
|
||||
if (currentToken == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class JsonNetSerializer : IMessageSerializer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Node accessor
|
||||
/// </summary>
|
||||
public struct NodeAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Value
|
||||
/// </summary>
|
||||
public object? Value { get; }
|
||||
/// <summary>
|
||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
||||
/// </summary>
|
||||
public int Type { get; }
|
||||
|
||||
private NodeAccessor(object? value, int type)
|
||||
{
|
||||
Value = value;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an int node accessor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, 0); }
|
||||
|
||||
/// <summary>
|
||||
/// Create a string node accessor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor String(string value) { return new NodeAccessor(value, 1); }
|
||||
|
||||
/// <summary>
|
||||
/// Create a property name node accessor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, 2); }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message access definition
|
||||
/// </summary>
|
||||
public struct MessagePath : IEnumerable<NodeAccessor>
|
||||
{
|
||||
private List<NodeAccessor> _path;
|
||||
|
||||
internal void Add(NodeAccessor node)
|
||||
{
|
||||
_path.Add(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessagePath()
|
||||
{
|
||||
_path = new List<NodeAccessor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new message path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Get()
|
||||
{
|
||||
return new MessagePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IEnumerable implementation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerator<NodeAccessor> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < _path.Count; i++)
|
||||
yield return _path[i];
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message path extension methods
|
||||
/// </summary>
|
||||
public static class MessagePathExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a string node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="propName"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Property(this MessagePath path, string propName)
|
||||
{
|
||||
path.Add(NodeAccessor.String(propName));
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a property name node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath PropertyName(this MessagePath path)
|
||||
{
|
||||
path.Add(NodeAccessor.PropertyName());
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Index(this MessagePath path, int index)
|
||||
{
|
||||
path.Add(NodeAccessor.Int(index));
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.Sockets.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message node type
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Array node
|
||||
/// </summary>
|
||||
Array,
|
||||
/// <summary>
|
||||
/// Object node
|
||||
/// </summary>
|
||||
Object,
|
||||
/// <summary>
|
||||
/// Value node
|
||||
/// </summary>
|
||||
Value
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
internal class PendingRequest
|
||||
{
|
||||
public Func<JToken, bool> Handler { get; }
|
||||
public JToken? Result { get; private set; }
|
||||
public bool Completed { get; private set; }
|
||||
public AsyncResetEvent Event { get; }
|
||||
public TimeSpan Timeout { get; }
|
||||
|
||||
private CancellationTokenSource cts;
|
||||
|
||||
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
|
||||
{
|
||||
Handler = handler;
|
||||
Event = new AsyncResetEvent(false, false);
|
||||
Timeout = timeout;
|
||||
|
||||
cts = new CancellationTokenSource(timeout);
|
||||
cts.Token.Register(Fail, false);
|
||||
}
|
||||
|
||||
public bool CheckData(JToken data)
|
||||
{
|
||||
if (Handler(data))
|
||||
{
|
||||
Result = data;
|
||||
Completed = true;
|
||||
Event.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Fail()
|
||||
{
|
||||
Completed = true;
|
||||
Event.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user