1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

84 Commits

Author SHA1 Message Date
JKorf 8b513e51b9 Updated version 2023-12-02 15:17:34 +01:00
JKorf d43b38a23a Fix requestBodyFormat parameter usage 2023-12-02 15:15:38 +01:00
JKorf 0987c0f9d1 Updated version 2023-12-02 14:22:12 +01:00
JKorf e2dde77023 Added DecimalStringWriter json converter, added support for specifying body content type on a per request basis 2023-12-02 14:20:41 +01:00
JKorf 104ac7caad Updated response logging, added RequestId to WebCallResult 2023-11-30 14:19:58 +01:00
JKorf 8788dd3deb Updated version 2023-10-28 15:19:11 +02:00
JKorf f64cc5e9cf Added additional helpers methods 2023-10-28 15:17:10 +02:00
JKorf 75d1bbc6e8 Updated examples package versions 2023-10-24 21:11:50 +02:00
JKorf b621aa7e65 Updated version 2023-10-24 18:44:22 +02:00
JKorf 9783108695 Added support for writing ints to EnumConverter 2023-10-24 18:41:38 +02:00
JKorf 6ba32fe280 Removed some things for internal use from interfaces 2023-10-12 22:25:25 +02:00
JKorf f75cc75bbc Added SerializerOptions helper class for setting default serializer, Added ParameterCollection for easier parameter definition, Added extra encryption helper methods on AuthenticationProvider 2023-10-12 22:03:09 +02:00
JKorf 2109b65a8e Fixed incorrect options docs 2023-10-09 21:36:12 +02:00
JKorf a472751638 Updated Examples 2023-10-09 20:43:08 +02:00
JKorf f08ed16f2a Updated version 2023-10-08 17:01:35 +02:00
JKorf 212d457a6a Added UpdateType to DataEvent model, added additional scenarios to BoolConverter, updated some logging 2023-10-08 16:59:43 +02:00
JKorf ac5f333766 Updated version 2023-09-23 21:16:09 +02:00
JKorf 640e4387c1 Added BoolConverter, added parameter for showing warning message to EnumConverter 2023-09-23 21:13:49 +02:00
JKorf a16b19019f Updated version 2023-09-18 20:13:00 +02:00
JKorf 2443f576ac Fix for concurrency exception 2023-09-18 20:02:01 +02:00
JKorf 4fd7e44015 Logging 2023-09-16 18:26:10 +02:00
JKorf a0a3bda1c5 Updated version 2023-09-11 20:59:44 +02:00
JKorf 6bda7a3c73 Fixed nullreference if no Retry-After is returned after with a ratelimit error 2023-09-11 20:57:46 +02:00
JKorf 69a7a714cd Updated rate limiters to support multiple instances 2023-09-11 20:16:50 +02:00
JKorf 48e2e6468e Updated version 2023-09-04 18:10:18 +02:00
JKorf 4017ac780f ArrayConverter update for handling exponent notation, EnumConverter fix for writing enum values 2023-09-04 18:09:01 +02:00
JKorf a55cd1bb13 Docs 2023-08-26 20:04:38 +02:00
JKorf b34129e148 Updated version 2023-08-24 21:25:37 +02:00
JKorf be25a68c9c Ratelimiting for socket requests 2023-08-24 20:51:17 +02:00
JKorf 468cd5e48e Added RetryAfter property for ratelimit errors, added parsing of rate limit return 2023-08-21 21:34:26 +02:00
JKorf 262c4e4aa5 Dont process unsubscribe if there are no subscriptions 2023-08-21 20:11:31 +02:00
Jan Korf 4ccff6461f Merge pull request #177 from ASolomatin/master
Ability for all Error derived classes to have Code and Data
2023-08-21 20:08:50 +02:00
JKorf 3bfa3ef389 Update index.md 2023-08-04 22:50:17 +02:00
JKorf 5238971bcc Update index.md 2023-08-04 22:44:09 +02:00
JKorf 2f5c904faf Added okx 2023-08-04 22:38:21 +02:00
JKorf c62775813f Updated version 2023-07-23 13:52:07 +02:00
JKorf f11b3754f0 Fix for proxy when not using DI 2023-07-23 10:01:13 +02:00
JKorf 3cbe0465e9 Docs 2023-07-11 21:38:48 +02:00
JKorf 5048aea722 Updated version 2023-07-05 21:58:35 +02:00
Aleksej Solomatin 8d35339ab2 Ability for all Error derived classes to have Code and Data
Proposal https://github.com/JKorf/CryptoExchange.Net/issues/176
2023-07-05 19:39:04 +03:00
Jkorf c3316a51e7 Added properties dictionary to socket connection 2023-07-05 17:10:43 +02:00
JKorf 7ecf37064b Updated version 2023-06-29 20:10:35 +02:00
JKorf 18954f4f53 Added optional log level parameter for trace logger 2023-06-29 20:07:26 +02:00
JKorf 00bc245102 Updated examples 2023-06-26 20:45:28 +02:00
JKorf 273cab9fdb Docs 2023-06-25 21:43:30 +02:00
JKorf 84d0f0ec9e Updated version 2023-06-25 20:25:02 +02:00
JKorf 690f2a63e5 Squashed commit of the following:
commit 90f285d7f6bcd926ce9ca3d5832b1d70a5eae6ab
Author: JKorf <jankorf91@gmail.com>
Date:   Sun Jun 25 19:51:12 2023 +0200

    Docs

commit 72187035c703d1402b37bd2f4c3e066706f28d67
Author: JKorf <jankorf91@gmail.com>
Date:   Sat Jun 24 16:02:53 2023 +0200

    docs

commit 8411977292f1fb0b6e0705b1ad675b79a5311d90
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jun 23 18:25:15 2023 +0200

    wip

commit cb7d33aad5d2751104c8b8a6c6eadbf0d36b672c
Author: JKorf <jankorf91@gmail.com>
Date:   Fri Jun 2 19:26:26 2023 +0200

    wip

commit 4359a2d05ea1141cff516dab18f364a6ca854e18
Author: JKorf <jankorf91@gmail.com>
Date:   Wed May 31 20:51:36 2023 +0200

    wip

commit c6adb1b2f728d143f6bd667139c619581122a3c9
Author: JKorf <jankorf91@gmail.com>
Date:   Mon May 1 21:13:47 2023 +0200

    wip

commit 7fee733f82fa6ff574030452f0955c9e817647dd
Author: JKorf <jankorf91@gmail.com>
Date:   Thu Apr 27 13:02:56 2023 +0200

    wip

commit f8057313ffc9b0c31effcda71d35d105ea390971
Author: JKorf <jankorf91@gmail.com>
Date:   Mon Apr 17 21:37:51 2023 +0200

    wip
2023-06-25 19:58:46 +02:00
JKorf 19cc020852 Updated version 2023-04-14 21:03:36 +02:00
JKorf 1d4353e6d1 Fixed potential collection modified exception 2023-04-14 21:02:21 +02:00
JKorf a02b3f88d7 Updated version 2023-04-01 19:00:22 +02:00
JKorf af44ca4c9f Logging 2023-04-01 18:58:52 +02:00
JKorf cf2b57bb96 Revert "Test some changes for robustness"
This reverts commit 1c33e297e7.
2023-04-01 18:55:48 +02:00
JKorf d0a2288910 Updated version 2023-03-18 14:58:09 +01:00
JKorf 89c11afc21 Fix Api key rate limit 2023-03-18 14:22:09 +01:00
JKorf da6ed580f1 Added CalculateTradableAmount to SymbolOrderBook 2023-03-18 10:01:38 +01:00
JKorf 1c33e297e7 Test some changes for robustness 2023-02-18 10:41:26 +01:00
JKorf 0005534a95 Updated version 2023-02-14 20:10:13 +01:00
JKorf 11650f7c1a Updated some interfaces, made time syncing methods nullable for apis not using it, added optional retry checking, removed private key from api credentials, added better support for api credentials subclasses 2023-02-13 21:18:45 +01:00
JKorf a222bb3f02 Fixed socket client options setting, added automatic unsubscribe if the subscription confirmation comes in after request timeout 2023-02-12 14:05:00 +01:00
JKorf 6361c5ef25 Added message on authentication exception 2023-02-08 21:55:56 +01:00
Jkorf 892e8a4508 Updated version 2022-12-08 11:59:02 +01:00
Jkorf 8336d373f3 Small fix for socket possibly reconnecting while it should close 2022-12-08 11:54:10 +01:00
Jkorf 71072680a8 Small fixes DelayAfterConnect option 2022-12-08 11:02:02 +01:00
Jan Korf e13f105019 Merge pull request #167 from alokym86/master
order parameters before constructing request
2022-12-08 11:01:07 +01:00
alokym86 401577451e order parameters
order the request parameters before AuthenticateRequest invocation
2022-12-05 14:55:01 +02:00
JKorf 5c41ef1ee4 Updated version 2022-11-14 21:23:18 +01:00
JKorf ad614830d1 Added optional delay after socket connection, added callback when reconnected socket to revitalize original request, fixed proxy setting socket 2022-11-13 19:47:33 +01:00
JKorf 3365837338 Updated tests 2022-11-13 15:31:28 +01:00
JKorf 66ac2972d6 wip 2022-11-02 17:59:33 +01:00
JKorf 0d3e05880a Wip client work 2022-10-31 21:41:30 +01:00
JKorf 997e71f3b7 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2022-10-28 20:02:37 +02:00
JKorf b0fca4587d Added null check array converter 2022-10-28 20:02:25 +02:00
Jan Korf c10671768d Merge pull request #160 from kulikov-dev/fix/155-format-conversion-error
Fixing exponential format parsing
2022-10-15 13:34:00 +02:00
kulikov-dev 91e8123679 Fixing issue 155 with exponential format parsing (https://github.com/JKorf/CryptoExchange.Net/issues/155) 2022-10-15 09:57:14 +06:00
JKorf 417cf2f9ac Fixed nullability warning 2022-07-31 21:55:26 +02:00
JKorf 277be7ab9b Updated version 2022-07-31 21:53:39 +02:00
JKorf 45f3459f59 Made DataEvent ctor public 2022-07-31 21:47:22 +02:00
JKorf 98dad4a8ed Added handling for websocket options not being supported when running on WebAssembly 2022-07-31 21:45:19 +02:00
JKorf 1e5f19271b Updated support docs 2022-07-31 14:51:47 +02:00
JKorf 8abeeb4cf0 Update Clients.md 2022-07-29 18:57:04 +02:00
JKorf cae0cd9ead Fixed EnumConverter serialization writing values without quotes 2022-07-27 21:26:17 +02:00
JKorf 811574ae01 Fixed websocket reconnecting too fast when reconnecting succeeds but resubscribing or authorization fails 2022-07-27 21:25:46 +02:00
JKorf 0ddecf7f8d Updated version 2022-07-19 19:13:26 +02:00
JKorf 5bcf50fb4d Fixed socket getting disconnected when no data timeout reached instead of being reconnected 2022-07-19 19:12:26 +02:00
99 changed files with 5003 additions and 3935 deletions
@@ -11,66 +11,6 @@ namespace CryptoExchange.Net.UnitTests
[TestFixture()] [TestFixture()]
public class BaseClientTests 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] [TestCase]
public void DeserializingValidJson_Should_GiveSuccessfulResult() public void DeserializingValidJson_Should_GiveSuccessfulResult()
{ {
@@ -78,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests
var client = new TestBaseClient(); var client = new TestBaseClient();
// act // act
var result = client.Deserialize<object>("{\"testProperty\": 123}"); var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// assert // assert
Assert.IsTrue(result.Success); Assert.IsTrue(result.Success);
@@ -91,7 +31,7 @@ namespace CryptoExchange.Net.UnitTests
var client = new TestBaseClient(); var client = new TestBaseClient();
// act // act
var result = client.Deserialize<object>("{\"testProperty\": 123"); var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert // assert
Assert.IsFalse(result.Success); Assert.IsFalse(result.Success);
@@ -113,7 +113,9 @@ namespace CryptoExchange.Net.UnitTests
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(), new List<KeyValuePair<string, IEnumerable<string>>>(),
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null,
"{}", "{}",
1,
"https://test.com/api", "https://test.com/api",
null, null,
HttpMethod.Get, HttpMethod.Get,
@@ -140,7 +142,9 @@ namespace CryptoExchange.Net.UnitTests
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
new List<KeyValuePair<string, IEnumerable<string>>>(), new List<KeyValuePair<string, IEnumerable<string>>>(),
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null,
"{}", "{}",
1,
"https://test.com/api", "https://test.com/api",
null, null,
HttpMethod.Get, HttpMethod.Get,
+48 -190
View File
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NUnit.Framework; using NUnit.Framework;
@@ -34,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
// act // act
// assert // assert
Assert.Throws(typeof(ArgumentException), Assert.Throws(typeof(ArgumentException),
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) }); () => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
} }
[Test] [Test]
@@ -57,239 +58,96 @@ namespace CryptoExchange.Net.UnitTests
public void TestApiOptionsAreSet() public void TestApiOptionsAreSet()
{ {
// arrange, act // arrange, act
var options = new TestClientOptions var options = new TestClientOptions();
{ options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
Api1Options = new RestApiClientOptions options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
{
ApiCredentials = new ApiCredentials("123", "456"),
BaseAddress = "http://test1.com"
},
Api2Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("789", "101"),
BaseAddress = "http://test2.com"
}
};
// assert // assert
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123"); Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456"); 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.Key.GetString(), "789");
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101"); 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] [Test]
public void TestClientUsesCorrectOptions() public void TestClientUsesCorrectOptions()
{ {
var client = new TestRestClient(new TestClientOptions() var client = new TestRestClient(options => {
{ options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
ApiCredentials = new ApiCredentials("123", "456"), options.ApiCredentials = new ApiCredentials("333", "444");
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
}); });
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111"); var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222"); var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123"); Assert.AreEqual(authProvider1.GetKey(), "111");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456"); Assert.AreEqual(authProvider1.GetSecret(), "222");
Assert.AreEqual(authProvider2.GetKey(), "333");
Assert.AreEqual(authProvider2.GetSecret(), "444");
} }
[Test] [Test]
public void TestClientUsesCorrectOptionsWithDefault() public void TestClientUsesCorrectOptionsWithDefault()
{ {
TestClientOptions.Default = new TestClientOptions() TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
{ TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
var client = new TestRestClient(); var client = new TestRestClient();
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111"); var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222"); var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123"); Assert.AreEqual(authProvider1.GetKey(), "111");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456"); Assert.AreEqual(authProvider1.GetSecret(), "222");
Assert.AreEqual(authProvider2.GetKey(), "123");
Assert.AreEqual(authProvider2.GetSecret(), "456");
} }
[Test] [Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault() public void TestClientUsesCorrectOptionsWithOverridingDefault()
{ {
TestClientOptions.Default = new TestClientOptions() TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
{ TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
ApiCredentials = new ApiCredentials("123", "456"),
Api1Options = new RestApiClientOptions
{
ApiCredentials = new ApiCredentials("111", "222")
}
};
var client = new TestRestClient(new TestClientOptions var client = new TestRestClient(options =>
{ {
Api1Options = new RestApiClientOptions options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
{ options.Environment = new TestEnvironment("Test", "https://test.test");
ApiCredentials = new ApiCredentials("333", "444")
},
Api2Options = new RestApiClientOptions()
{
BaseAddress = "http://test.com"
}
}); });
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333"); var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444"); var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123"); Assert.AreEqual(authProvider1.GetKey(), "333");
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456"); Assert.AreEqual(authProvider1.GetSecret(), "444");
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com"); 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> /// <summary>
/// Default options for the futures client /// Default options for the futures client
/// </summary> /// </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> /// <summary>
/// The default receive window for requests /// The default receive window for requests
/// </summary> /// </summary>
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/"); public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
public RestApiClientOptions Api1Options
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Copy()
{ {
get => _api1Options; var options = Copy<TestClientOptions>();
set => _api1Options = new RestApiClientOptions(_api1Options, value); options.Api1Options = Api1Options.Copy<RestApiOptions>();
} options.Api2Options = Api2Options.Copy<RestApiOptions>();
return options;
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);
} }
} }
} }
+28 -69
View File
@@ -11,7 +11,6 @@ using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using System.Threading; using System.Threading;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -28,7 +27,7 @@ namespace CryptoExchange.Net.UnitTests
client.SetResponse(JsonConvert.SerializeObject(expected), out _); client.SetResponse(JsonConvert.SerializeObject(expected), out _);
// act // act
var result = client.Request<TestObject>().Result; var result = client.Api1.Request<TestObject>().Result;
// assert // assert
Assert.IsTrue(result.Success); Assert.IsTrue(result.Success);
@@ -43,7 +42,7 @@ namespace CryptoExchange.Net.UnitTests
client.SetResponse("{\"property\": 123", out _); client.SetResponse("{\"property\": 123", out _);
// act // act
var result = client.Request<TestObject>().Result; var result = client.Api1.Request<TestObject>().Result;
// assert // assert
Assert.IsFalse(result.Success); Assert.IsFalse(result.Success);
@@ -58,7 +57,7 @@ namespace CryptoExchange.Net.UnitTests
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request"); client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
// act // act
var result = await client.Request<TestObject>(); var result = await client.Api1.Request<TestObject>();
// assert // assert
Assert.IsFalse(result.Success); Assert.IsFalse(result.Success);
@@ -73,7 +72,7 @@ namespace CryptoExchange.Net.UnitTests
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest); client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act // act
var result = await client.Request<TestObject>(); var result = await client.Api1.Request<TestObject>();
// assert // assert
Assert.IsFalse(result.Success); Assert.IsFalse(result.Success);
@@ -91,7 +90,7 @@ namespace CryptoExchange.Net.UnitTests
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest); client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act // act
var result = await client.Request<TestObject>(); var result = await client.Api2.Request<TestObject>();
// assert // assert
Assert.IsFalse(result.Success); Assert.IsFalse(result.Success);
@@ -106,23 +105,16 @@ namespace CryptoExchange.Net.UnitTests
{ {
// arrange // arrange
// act // act
var client = new TestRestClient(new TestClientOptions() var options = new TestClientOptions();
{ options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
Api1Options = new RestApiClientOptions options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
{ options.RequestTimeout = TimeSpan.FromMinutes(1);
BaseAddress = "http://test.address.com", var client = new TestBaseClient(options);
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
RateLimitingBehaviour = RateLimitingBehaviour.Fail
},
RequestTimeout = TimeSpan.FromMinutes(1)
});
// assert // 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.RateLimiters.Count == 1);
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail); 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 [TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
@@ -136,19 +128,13 @@ namespace CryptoExchange.Net.UnitTests
{ {
// arrange // arrange
// act // act
var client = new TestRestClient(new TestClientOptions() var client = new TestRestClient();
{
Api1Options = new RestApiClientOptions
{
BaseAddress = "http://test.address.com"
}
});
client.Api1.SetParameterPosition(new HttpMethod(method), pos); client.Api1.SetParameterPosition(new HttpMethod(method), pos);
client.SetResponse("{}", out var request); 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" }, { "TestParam1", "Value1" },
{ "TestParam2", 2 }, { "TestParam2", 2 },
@@ -175,20 +161,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(1, 2)] [TestCase(1, 2)]
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds) public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds)); rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
for (var i = 0; i < requests + 1; i++) 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); Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
} }
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); 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); Assert.IsTrue(result2.Data == 0);
} }
@@ -199,15 +182,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/", true)] [TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting) public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1)); rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++) 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; bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected); Assert.IsTrue(expected);
} }
@@ -218,14 +198,11 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test", "/sapi/", false)] [TestCase("/sapi/test", "/sapi/", false)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting) public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true); 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 result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), 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 result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0); Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0); Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
} }
@@ -236,20 +213,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(1, 2)] [TestCase(1, 2)]
public async Task EndpointRateLimiterBasics(int requests, double perSeconds) public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds)); rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
for (var i = 0; i < requests + 1; i++) 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); Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
} }
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); 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); Assert.IsTrue(result2.Data == 0);
} }
@@ -258,15 +232,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test/123", false)] [TestCase("/sapi/test/123", false)]
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited) public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1)); rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++) 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; bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected); Assert.IsTrue(expected);
} }
@@ -278,15 +249,12 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test23", false)] [TestCase("/sapi/test23", false)]
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited) public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1)); rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
for (var i = 0; i < 2; i++) 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; bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
Assert.IsTrue(expected); Assert.IsTrue(expected);
} }
@@ -315,14 +283,11 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)] [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) 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(); var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false); 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 result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), 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 result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
Assert.IsTrue(result1.Data == 0); Assert.IsTrue(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0); Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
} }
@@ -332,14 +297,11 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/", "/sapi/test2", true)] [TestCase("/", "/sapi/test2", true)]
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited) public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
{ {
var log = new Log("Test");
log.Level = LogLevel.Trace;
var rateLimiter = new RateLimiter(); var rateLimiter = new RateLimiter();
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1)); rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, 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(log, endpoint2, HttpMethod.Get, true, "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(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.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)] [TestCase("/sapi/test", true, true, false, true)]
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited) 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(); var rateLimiter = new RateLimiter();
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal); rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1)); rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "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(log, endpoint, HttpMethod.Get, signed2, "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(result1.Data == 0);
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0); Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
} }
@@ -17,19 +17,16 @@ namespace CryptoExchange.Net.UnitTests
{ {
//arrange //arrange
//act //act
var client = new TestSocketClient(new TestOptions() var client = new TestSocketClient(options =>
{ {
SubOptions = new RestApiClientOptions options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
{ options.SubOptions.MaxSocketConnections = 1;
BaseAddress = "http://test.address.com"
},
ReconnectInterval = TimeSpan.FromSeconds(6)
}); });
//assert //assert
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com"); Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6); Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
} }
[TestCase(true)] [TestCase(true)]
@@ -42,7 +39,7 @@ namespace CryptoExchange.Net.UnitTests
socket.CanConnect = canConnect; socket.CanConnect = canConnect;
//act //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
Assert.IsTrue(connectResult.Success == canConnect); Assert.IsTrue(connectResult.Success == canConnect);
@@ -52,12 +49,14 @@ namespace CryptoExchange.Net.UnitTests
public void SocketMessages_Should_BeProcessedInDataHandlers() public void SocketMessages_Should_BeProcessedInDataHandlers()
{ {
// arrange // 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(); var socket = client.CreateSocket();
socket.ShouldReconnect = true; socket.ShouldReconnect = true;
socket.CanConnect = true; socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow; 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); var rstEvent = new ManualResetEvent(false);
JToken result = null; JToken result = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) => sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
@@ -65,7 +64,7 @@ namespace CryptoExchange.Net.UnitTests
result = messageEvent.JsonData; result = messageEvent.JsonData;
rstEvent.Set(); rstEvent.Set();
})); }));
client.ConnectSocketSub(sub); client.SubClient.ConnectSocketSub(sub);
// act // act
socket.InvokeMessage("{\"property\": 123}"); socket.InvokeMessage("{\"property\": 123}");
@@ -80,12 +79,15 @@ namespace CryptoExchange.Net.UnitTests
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{ {
// arrange // 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(); var socket = client.CreateSocket();
socket.ShouldReconnect = true; socket.ShouldReconnect = true;
socket.CanConnect = true; socket.CanConnect = true;
socket.DisconnectTime = DateTime.UtcNow; 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); var rstEvent = new ManualResetEvent(false);
string original = null; string original = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) => sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
@@ -93,7 +95,7 @@ namespace CryptoExchange.Net.UnitTests
original = messageEvent.OriginalData; original = messageEvent.OriginalData;
rstEvent.Set(); rstEvent.Set();
})); }));
client.ConnectSocketSub(sub); client.SubClient.ConnectSocketSub(sub);
// act // act
socket.InvokeMessage("{\"property\": 123}"); socket.InvokeMessage("{\"property\": 123}");
@@ -107,11 +109,13 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingStream_Should_CloseTheSocket() public void UnsubscribingStream_Should_CloseTheSocket()
{ {
// arrange // 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(); var socket = client.CreateSocket();
socket.CanConnect = true; socket.CanConnect = true;
var sub = new SocketConnection(client, null, socket, null); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
client.ConnectSocketSub(sub); client.SubClient.ConnectSocketSub(sub);
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { }); var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
var ups = new UpdateSubscription(sub, us); var ups = new UpdateSubscription(sub, us);
sub.AddSubscription(us); sub.AddSubscription(us);
@@ -127,15 +131,21 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingAll_Should_CloseAllSockets() public void UnsubscribingAll_Should_CloseAllSockets()
{ {
// arrange // 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 socket1 = client.CreateSocket();
var socket2 = client.CreateSocket(); var socket2 = client.CreateSocket();
socket1.CanConnect = true; socket1.CanConnect = true;
socket2.CanConnect = true; socket2.CanConnect = true;
var sub1 = new SocketConnection(client, null, socket1, null); var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
var sub2 = new SocketConnection(client, null, socket2, null); var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
client.ConnectSocketSub(sub1); client.SubClient.ConnectSocketSub(sub1);
client.ConnectSocketSub(sub2); client.SubClient.ConnectSocketSub(sub2);
var us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
sub1.AddSubscription(us1);
sub2.AddSubscription(us2);
var ups1 = new UpdateSubscription(sub1, us1);
var ups2 = new UpdateSubscription(sub2, us2);
// act // act
client.UnsubscribeAllAsync().Wait(); client.UnsubscribeAllAsync().Wait();
@@ -149,13 +159,13 @@ namespace CryptoExchange.Net.UnitTests
public void FailingToConnectSocket_Should_ReturnError() public void FailingToConnectSocket_Should_ReturnError()
{ {
// arrange // 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(); var socket = client.CreateSocket();
socket.CanConnect = false; socket.CanConnect = false;
var sub = new SocketConnection(client, null, socket, null); var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// act // act
var connectResult = client.ConnectSocketSub(sub); var connectResult = client.SubClient.ConnectSocketSub(sub1);
// assert // assert
Assert.IsFalse(connectResult.Success); Assert.IsFalse(connectResult.Success);
@@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.OrderBook; using CryptoExchange.Net.OrderBook;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using NUnit.Framework; using NUnit.Framework;
@@ -17,8 +18,9 @@ namespace CryptoExchange.Net.UnitTests
private class TestableSymbolOrderBook : SymbolOrderBook 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) public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{ {
Status = OrderBookStatus.Synced; Status = OrderBookStatus.Synced;
base.bids.Clear(); base._bids.Clear();
foreach (var bid in bids) foreach (var bid in bids)
base.bids.Add(bid.Price, bid); base._bids.Add(bid.Price, bid);
base.asks.Clear(); base._asks.Clear();
foreach (var ask in asks) 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.06666667m, resultBids2.Data);
Assert.AreEqual(1.23333333m, resultAsks2.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);
}
} }
} }
@@ -1,31 +1,50 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
public class TestBaseClient: BaseClient 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) public void Log(LogLevel verbosity, string data)
{ {
log.Write(verbosity, data); _logger.Log(verbosity, data);
}
} }
public CallResult<T> Deserialize<T>(string data) public class TestSubClient : RestApiClient
{
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{ {
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 public class TestAuthProvider : AuthenticationProvider
@@ -41,9 +60,7 @@ namespace CryptoExchange.Net.UnitTests
headers = new Dictionary<string, string>(); headers = new Dictionary<string, string>();
} }
public override string Sign(string toSign) public string GetKey() => _credentials.Key.GetString();
{ public string GetSecret() => _credentials.Secret.GetString();
return toSign;
}
} }
} }
@@ -12,6 +12,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using System.Collections.Generic; using System.Collections.Generic;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -20,15 +22,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public TestRestApi1Client Api1 { get; } public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { 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) public void SetResponse(string responseData, out IRequest requestObj)
@@ -50,7 +59,16 @@ 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.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); 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);
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>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => .Callback<HttpMethod, Uri, int>((method, uri, id) =>
{ {
@@ -71,7 +89,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>()); request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we); 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>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object); .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.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); 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>())) 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)) .Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object); .Returns(request.Object);
}
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class factory = Mock.Get(Api2.RequestFactory);
{ factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct); .Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
} .Returns(request.Object);
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);
} }
} }
public class TestRestApi1Client : RestApiClient 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) public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
@@ -122,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position; ParameterPositions[method] = position;
} }
public override TimeSpan GetTimeOffset() public override TimeSpan? GetTimeOffset()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -143,12 +172,24 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public class TestRestApi2Client : RestApiClient 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(); 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 class ParseErrorTestRestClient: TestRestClient
{ {
public ParseErrorTestRestClient() { } public ParseErrorTestRestClient() { }
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
protected override Error ParseErrorResponse(JToken error)
{
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
}
} }
} }
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public event Action OnReconnected; public event Action OnReconnected;
public event Action OnReconnecting; public event Action OnReconnecting;
#pragma warning restore 0067 #pragma warning restore 0067
public event Action<int> OnRequestSent;
public event Action<string> OnMessage; public event Action<string> OnMessage;
public event Action<Exception> OnError; public event Action<Exception> OnError;
public event Action OnOpen; public event Action OnOpen;
@@ -69,10 +70,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return Task.FromResult(CanConnect); return Task.FromResult(CanConnect);
} }
public void Send(string data) public void Send(int requestId, string data, int weight)
{ {
if(!Connected) if(!Connected)
throw new Exception("Socket not connected"); throw new Exception("Socket not connected");
OnRequestSent?.Invoke(requestId);
} }
public void Reset() public void Reset()
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -14,23 +16,73 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
public TestSubSocketClient SubClient { get; } 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() public TestSocket CreateSocket()
{ {
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket()); Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
return (TestSocket)CreateSocket("https://localhost:123/"); 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) public CallResult<bool> ConnectSocketSub(SocketConnection sub)
{ {
return ConnectSocketAsync(sub).Result; return ConnectSocketAsync(sub).Result;
@@ -67,21 +119,4 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException(); 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);
}
} }
@@ -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();
}
}
}
+5
View File
@@ -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; } public SecureString? Secret { get; }
/// <summary> /// <summary>
/// The private key to authenticate requests /// Type of the credentials
/// </summary> /// </summary>
public PrivateKey? PrivateKey { get; } public ApiCredentialsType CredentialType { get; }
/// <summary> /// <summary>
/// Create Api credentials providing a private key for authentication /// Create Api credentials providing an api key and secret for authentication
/// </summary> /// </summary>
/// <param name="privateKey">The private key used for signing</param> /// <param name="key">The api key used for identification</param>
public ApiCredentials(PrivateKey privateKey) /// <param name="secret">The api secret used for signing</param>
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
{ {
PrivateKey = privateKey;
} }
/// <summary> /// <summary>
@@ -40,11 +40,13 @@ namespace CryptoExchange.Net.Authentication
/// </summary> /// </summary>
/// <param name="key">The api key used for identification</param> /// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</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) if (key == null || secret == null)
throw new ArgumentException("Key and secret can't be null/empty"); throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
Key = key; Key = key;
Secret = secret; Secret = secret;
} }
@@ -54,11 +56,22 @@ namespace CryptoExchange.Net.Authentication
/// </summary> /// </summary>
/// <param name="key">The api key used for identification</param> /// <param name="key">The api key used for identification</param>
/// <param name="secret">The api secret used for signing</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)) if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty"); throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialsType;
Key = key.ToSecureString(); Key = key.ToSecureString();
Secret = secret.ToSecureString(); Secret = secret.ToSecureString();
} }
@@ -69,11 +82,8 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
public virtual ApiCredentials Copy() public virtual ApiCredentials Copy()
{ {
if (PrivateKey == null)
// Use .GetString() to create a copy of the SecureString // Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString()); return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
else
return new ApiCredentials(PrivateKey!.Copy());
} }
/// <summary> /// <summary>
@@ -123,7 +133,6 @@ namespace CryptoExchange.Net.Authentication
{ {
Key?.Dispose(); Key?.Dispose();
Secret?.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> /// <summary>
/// Base class for authentication providers /// Base class for authentication providers
/// </summary> /// </summary>
public abstract class AuthenticationProvider public abstract class AuthenticationProvider : IDisposable
{ {
/// <summary> /// <summary>
/// The provided credentials /// Provided credentials
/// </summary> /// </summary>
public ApiCredentials Credentials { get; } protected readonly ApiCredentials _credentials;
/// <summary> /// <summary>
/// Byte representation of the secret
/// </summary> /// </summary>
protected byte[] _sBytes; protected byte[] _sBytes;
@@ -32,7 +33,7 @@ namespace CryptoExchange.Net.Authentication
if (credentials.Secret == null) if (credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed"); throw new ArgumentException("ApiKey/Secret needed");
Credentials = credentials; _credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString()); _sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
} }
@@ -73,6 +74,17 @@ namespace CryptoExchange.Net.Authentication
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); 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> /// <summary>
/// SHA256 sign the data and return the hash /// SHA256 sign the data and return the hash
/// </summary> /// </summary>
@@ -86,6 +98,19 @@ namespace CryptoExchange.Net.Authentication
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> /// <summary>
/// SHA384 sign the data and return the hash /// SHA384 sign the data and return the hash
/// </summary> /// </summary>
@@ -99,6 +124,41 @@ namespace CryptoExchange.Net.Authentication
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); 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> /// <summary>
/// SHA512 sign the data and return the hash /// SHA512 sign the data and return the hash
/// </summary> /// </summary>
@@ -112,6 +172,41 @@ namespace CryptoExchange.Net.Authentication
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); 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> /// <summary>
/// MD5 sign the data and return the hash /// MD5 sign the data and return the hash
/// </summary> /// </summary>
@@ -126,28 +221,70 @@ namespace CryptoExchange.Net.Authentication
} }
/// <summary> /// <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> /// </summary>
/// <param name="data">Data to sign</param> /// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param> /// <param name="outputType">String type</param>
/// <returns></returns> /// <returns></returns>
protected string SignHMACSHA256(string data, SignOutputType? outputType = null) 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); 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); 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> /// <summary>
/// HMACSHA384 sign the data and return the hash /// HMACSHA384 sign the data and return the hash
/// </summary> /// </summary>
/// <param name="data">Data to sign</param> /// <param name="data">Data to sign</param>
/// <param name="outputType">String type</param> /// <param name="outputType">String type</param>
/// <returns></returns> /// <returns></returns>
protected string SignHMACSHA384(string data, SignOutputType? outputType = null) protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
{ {
using var encryptor = new HMACSHA384(_sBytes); 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); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
@@ -174,23 +311,80 @@ namespace CryptoExchange.Net.Authentication
} }
/// <summary> /// <summary>
/// Sign a string /// SHA256 sign the data
/// </summary> /// </summary>
/// <param name="toSign"></param> /// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns> /// <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> /// <summary>
/// Sign a byte array /// SHA384 sign the data
/// </summary> /// </summary>
/// <param name="toSign"></param> /// <param name="data"></param>
/// <param name="outputType"></param>
/// <returns></returns> /// <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> /// <summary>
@@ -223,7 +417,7 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static DateTime GetTimestamp(RestApiClient apiClient) protected static DateTime GetTimestamp(RestApiClient apiClient)
{ {
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!; return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
} }
/// <summary> /// <summary>
@@ -235,5 +429,26 @@ namespace CryptoExchange.Net.Authentication
{ {
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture); 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();
}
}
}
+259 -35
View File
@@ -1,37 +1,41 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
/// <summary> /// <summary>
/// Base API for all API clients /// Base API for all API clients
/// </summary> /// </summary>
public abstract class BaseApiClient: IDisposable public abstract class BaseApiClient : IDisposable, IBaseApiClient
{ {
private ApiCredentials? _apiCredentials; /// <summary>
private AuthenticationProvider? _authenticationProvider; /// Logger
private bool _created; /// </summary>
private bool _disposing; protected ILogger _logger;
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
/// <summary> /// <summary>
/// The authentication provider for this API client. (null if no credentials are set) /// The authentication provider for this API client. (null if no credentials are set)
/// </summary> /// </summary>
public AuthenticationProvider? AuthenticationProvider public AuthenticationProvider? AuthenticationProvider { get; private set; }
{
get
{
if (!_created && !_disposing && _apiCredentials != null)
{
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
_created = true;
}
return _authenticationProvider;
}
}
/// <summary> /// <summary>
/// Where to put the parameters for requests with different Http methods /// Where to put the parameters for requests with different Http methods
@@ -65,25 +69,53 @@ namespace CryptoExchange.Net
public string requestBodyEmptyContent = "{}"; public string requestBodyEmptyContent = "{}";
/// <summary> /// <summary>
/// The base address for this API client /// The environment this client communicates to
/// </summary> /// </summary>
internal protected string BaseAddress { get; } public string BaseAddress { get; }
/// <summary> /// <summary>
/// Api client options /// Output the original string data along with the deserialized object
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="options">Client options</param> /// <param name="logger">Logger</param>
/// <param name="apiOptions">Api client options</param> /// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions) /// <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; _logger = logger;
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
BaseAddress = apiOptions.BaseAddress; ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
if (apiCredentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
}
} }
/// <summary> /// <summary>
@@ -94,21 +126,213 @@ namespace CryptoExchange.Net
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials); protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc /> /// <inheritdoc />
public void SetApiCredentials(ApiCredentials credentials) public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{ {
_apiCredentials = credentials?.Copy(); if (credentials != null)
_created = false; {
_authenticationProvider = 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);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
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);
_logger.Log(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]";
}
}
_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> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public void Dispose() public virtual void Dispose()
{ {
_disposing = true; _disposing = true;
_apiCredentials?.Dispose(); AuthenticationProvider?.Dispose();
AuthenticationProvider?.Credentials?.Dispose();
} }
} }
} }
+39 -247
View File
@@ -1,15 +1,10 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -22,54 +17,58 @@ namespace CryptoExchange.Net
/// The name of the API the client is for /// The name of the API the client is for
/// </summary> /// </summary>
internal string Name { get; } internal string Name { get; }
/// <summary> /// <summary>
/// Api clients in this client /// Api clients in this client
/// </summary> /// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>(); internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary> /// <summary>
/// The log object /// The log object
/// </summary> /// </summary>
protected internal Log log; protected internal ILogger _logger;
/// <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
});
/// <summary> /// <summary>
/// Provided client options /// Provided client options
/// </summary> /// </summary>
public BaseClientOptions ClientOptions { get; } public ExchangeOptions ClientOptions { get; private set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger">Logger</param>
/// <param name="name">The name of the API this client is for</param> /// <param name="name">The name of the API this client is for</param>
/// <param name="options">The options for this client</param> #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected BaseClient(string name, BaseClientOptions options) 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); _logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
log.UpdateWriters(options.LogWriters);
log.Level = options.LogLevel;
options.OnLoggingChanged += HandleLogConfigChange;
ClientOptions = options;
Name = 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> /// <summary>
@@ -78,227 +77,20 @@ namespace CryptoExchange.Net
/// <param name="apiClient">The client</param> /// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient 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); ApiClients.Add(apiClient);
return 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> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public virtual void Dispose()
{ {
log.Write(LogLevel.Debug, "Disposing client"); _logger.Log(LogLevel.Debug, "Disposing client");
ClientOptions.OnLoggingChanged -= HandleLogConfigChange;
foreach (var client in ApiClients) foreach (var client in ApiClients)
client.Dispose(); client.Dispose();
} }
+2 -474
View File
@@ -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.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -22,475 +9,16 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient 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 /> /// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade); 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> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</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(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
{ {
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());
} }
} }
} }
+19 -721
View File
@@ -1,17 +1,11 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -21,684 +15,27 @@ namespace CryptoExchange.Net
public abstract class BaseSocketClient: BaseClient, ISocketClient public abstract class BaseSocketClient: BaseClient, ISocketClient
{ {
#region fields #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> /// <summary>
/// If client is disposing /// If client is disposing
/// </summary> /// </summary>
protected bool disposing; 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; }
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
{
get
{
if (!socketConnections.Any())
return 0;
return socketConnections.Sum(s => s.Value.IncomingKbps);
}
}
/// <inheritdoc /> /// <inheritdoc />
public int CurrentConnections => socketConnections.Count; public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc /> /// <inheritdoc />
public int CurrentSubscriptions public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
{
get
{
if (!socketConnections.Any())
return 0;
return socketConnections.Sum(s => s.Value.SubscriptionCount);
}
}
/// <summary>
/// Client options
/// </summary>
public new BaseSocketClientOptions ClientOptions { get; }
#endregion #endregion
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger">Logger</param>
/// <param name="name">The name of the API this client is for</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(ILoggerFactory? logger, string name) : base(logger, name)
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
{ {
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>
/// Get the url to reconnect to after losing a connection
/// </summary>
/// <param name="apiClient"></param>
/// <param name="connection"></param>
/// <returns></returns>
public virtual Task<Uri?> GetReconnectUriAsync(SocketApiClient apiClient, SocketConnection connection)
{
return Task.FromResult<Uri?>(connection.ConnectionUri);
}
/// <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> /// <summary>
@@ -708,25 +45,14 @@ namespace CryptoExchange.Net
/// <returns></returns> /// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId) public virtual async Task UnsubscribeAsync(int subscriptionId)
{ {
SocketSubscription? subscription = null; foreach(var socket in ApiClients.OfType<SocketApiClient>())
SocketConnection? connection = null;
foreach(var socket in socketConnections.Values.ToList())
{ {
subscription = socket.GetSubscription(subscriptionId); var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (subscription != null) if (result)
{
connection = socket;
break; break;
} }
} }
if (subscription == null || connection == null)
return;
log.Write(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
await connection.CloseAsync(subscription).ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Unsubscribe an update subscription /// Unsubscribe an update subscription
/// </summary> /// </summary>
@@ -737,7 +63,7 @@ namespace CryptoExchange.Net
if (subscription == null) if (subscription == null)
throw new ArgumentNullException(nameof(subscription)); 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); await subscription.CloseAsync().ConfigureAwait(false);
} }
@@ -747,13 +73,9 @@ namespace CryptoExchange.Net
/// <returns></returns> /// <returns></returns>
public virtual async Task UnsubscribeAllAsync() public virtual async Task UnsubscribeAllAsync()
{ {
log.Write(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
var tasks = new List<Task>(); var tasks = new List<Task>();
{ foreach (var client in ApiClients.OfType<SocketApiClient>())
var socketList = socketConnections.Values; tasks.Add(client.UnsubscribeAllAsync());
foreach (var sub in socketList)
tasks.Add(sub.CloseAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
} }
@@ -764,14 +86,12 @@ namespace CryptoExchange.Net
/// <returns></returns> /// <returns></returns>
public virtual async Task ReconnectAsync() 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>(); var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{ {
var socketList = socketConnections.Values; tasks.Add(client.ReconnectAsync());
foreach (var sub in socketList)
tasks.Add(sub.TriggerReconnectAsync());
} }
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
} }
@@ -780,32 +100,10 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public string GetSubscriptionsState() public string GetSubscriptionsState()
{ {
var sb = new StringBuilder(); var result = new StringBuilder();
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}"); foreach(var client in ApiClients.OfType<SocketApiClient>())
foreach(var connection in socketConnections) result.AppendLine(client.GetSubscriptionsState());
{ return result.ToString();
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}");
}
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();
} }
} }
} }
+553 -21
View File
@@ -1,74 +1,606 @@
using System; using System;
using System.Collections.Generic; 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
/// <summary> /// <summary>
/// Base rest API client for interacting with a REST API /// Base rest API client for interacting with a REST API
/// </summary> /// </summary>
public abstract class RestApiClient: BaseApiClient public abstract class RestApiClient : BaseApiClient, IRestApiClient
{ {
/// <summary> /// <inheritdoc />
/// Get time sync info for an API client public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// </summary>
/// <returns></returns>
public abstract TimeSyncInfo GetTimeSyncInfo();
/// <summary> /// <inheritdoc />
/// Get time offset for an API client public abstract TimeSyncInfo? GetTimeSyncInfo();
/// </summary>
/// <returns></returns>
public abstract TimeSpan GetTimeOffset();
/// <summary> /// <inheritdoc />
/// Total amount of requests made with this API client public abstract TimeSpan? GetTimeOffset();
/// </summary>
/// <inheritdoc />
public int TotalRequestsMade { get; set; } public int TotalRequestsMade { get; set; }
/// <summary> /// <summary>
/// Options for this client /// Request headers to be sent with each request
/// </summary> /// </summary>
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options; protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary> /// <summary>
/// List of rate limiters /// List of rate limiters
/// </summary> /// </summary>
internal IEnumerable<IRateLimiter> RateLimiters { get; } internal IEnumerable<IRateLimiter> RateLimiters { get; }
/// <inheritdoc />
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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="options">The base client options</param>
/// <param name="apiOptions">The Api 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>(); var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters) foreach (var rateLimiter in apiOptions.RateLimiters)
rateLimiters.Add(rateLimiter); rateLimiters.Add(rateLimiter);
RateLimiters = rateLimiters; 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, $"[{result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.Log(LogLevel.Debug, $"[{result.RequestId}] 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, $"[{result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.Log(LogLevel.Debug, $"[{result.RequestId}] 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, $"[{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, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new CallResult<IRequest>(new NoApiCredentialsError());
}
_logger.Log(LogLevel.Information, $"[{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, $"[{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> /// <summary>
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues /// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary> /// </summary>
/// <returns>Server time</returns> /// <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() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); 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 (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{ {
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)) if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
{ {
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);
} }
var localTime = DateTime.UtcNow; var localTime = DateTime.UtcNow;
@@ -97,7 +629,7 @@ namespace CryptoExchange.Net
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);
} }
} }
} }
+823 -3
View File
@@ -1,19 +1,839 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
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 static CryptoExchange.Net.Objects.RateLimiter;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
/// <summary> /// <summary>
/// Base socket API client for interaction with a websocket API /// Base socket API client for interaction with a websocket API
/// </summary> /// </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>
/// 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 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 rate limiters
/// </summary>
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <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.SubscriptionCount);
}
}
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
#endregion
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// <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>
/// 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="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>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
{
return SubscribeAsync(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="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>(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(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)
{
_logger.Log(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)
{
_logger.Log(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)
{
_logger.Log(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 () =>
{
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
_logger.Log(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.RequestTimeout, subscription, 1, 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="request">The request to send, will be serialized to json</param>
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
/// <param name="weight">Weight of the request</param>
/// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated, int weight = 1)
{
return QueryAsync<T>(BaseAddress, request, authenticated, weight);
}
/// <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="request">The request to send</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="weight">Weight of the request</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated, int weight = 1)
{
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, 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)
{
_logger.Log(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, weight).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>
/// <param name="weight">The weight of the query</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request, int weight)
{
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, weight, 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 (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return new CallResult<bool>(true);
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} Attempting to authenticate");
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
{
_logger.Log(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);
}
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} authenticated");
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, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp, null));
return;
}
var desResult = Deserialize<T>(messageEvent.JsonData);
if (!desResult)
{
_logger.Log(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
return;
}
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp, null));
}
var subscription = request == null
? SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier!, userSubscription, authenticated, InternalHandler)
: SocketSubscription.CreateForRequest(ExchangeHelpers.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(ExchangeHelpers.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="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 original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// </summary>
/// <param name="request">The original request</param>
/// <returns></returns>
protected internal virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
{
return Task.FromResult(new CallResult<object>(request));
}
/// <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.SubscriptionCount).FirstOrDefault();
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
if (result != null)
{
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? 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(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 kvp in genericHandlers)
{
var handler = SocketSubscription.CreateForIdentifier(ExchangeHelpers.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,
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, $"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>
protected 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;
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
try
{
socketConnection.Send(ExchangeHelpers.NextId(), obj, 1);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
}
}
}
});
}
/// <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)
{
SocketSubscription? 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, $"Socket {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, $"Socket {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.SubscriptionCount);
if (sum == 0)
return;
_logger.Log(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());
}
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($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
foreach (var connection in socketConnections)
{
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}");
}
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)
{
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
_ = UnsubscribeAllAsync();
}
semaphoreSlim?.Dispose();
base.Dispose();
}
} }
} }
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.Converters
/// </summary> /// </summary>
public class ArrayConverter : JsonConverter public class ArrayConverter : JsonConverter
{ {
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, 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>(); private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
@@ -28,6 +28,9 @@ namespace CryptoExchange.Net.Converters
/// <inheritdoc /> /// <inheritdoc />
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
if (reader.TokenType == JsonToken.Null)
return null;
if (objectType == typeof(JToken)) if (objectType == typeof(JToken))
return JToken.Load(reader); return JToken.Load(reader);
@@ -97,18 +100,30 @@ namespace CryptoExchange.Net.Converters
} }
if (value != null && property.PropertyType.IsInstanceOfType(value)) if (value != null && property.PropertyType.IsInstanceOfType(value))
{
property.SetValue(result, value); property.SetValue(result, value);
}
else else
{ {
if (value is JToken token) if (value is JToken token)
{
if (token.Type == JTokenType.Null) if (token.Type == JTokenType.Null)
value = null; value = null;
if ((property.PropertyType == typeof(decimal) if (token.Type == JTokenType.Float)
|| property.PropertyType == typeof(decimal?)) value = token.Value<decimal>();
&& (value != null && value.ToString().Contains("e"))) }
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); property.SetValue(result, dec);
} }
else else
@@ -172,10 +187,10 @@ namespace CryptoExchange.Net.Converters
} }
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute => 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 => 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> /// <summary>
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Converters
/// The enum->string mapping /// The enum->string mapping
/// </summary> /// </summary>
protected abstract List<KeyValuePair<T, string>> Mapping { get; } protected abstract List<KeyValuePair<T, string>> Mapping { get; }
private readonly bool quotes; private readonly bool _quotes;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Converters
/// <param name="useQuotes"></param> /// <param name="useQuotes"></param>
protected BaseConverter(bool useQuotes) protected BaseConverter(bool useQuotes)
{ {
quotes = useQuotes; _quotes = useQuotes;
} }
/// <inheritdoc /> /// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
var stringValue = value == null? null: GetValue((T) value); var stringValue = value == null? null: GetValue((T) value);
if (quotes) if (_quotes)
writer.WriteValue(stringValue); writer.WriteValue(stringValue);
else else
writer.WriteRawValue(stringValue); writer.WriteRawValue(stringValue);
@@ -0,0 +1,71 @@
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)
{
switch (reader.Value?.ToString().ToLower().Trim())
{
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 public class DateTimeConverter: JsonConverter
{ {
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
@@ -134,7 +134,7 @@ namespace CryptoExchange.Net.Converters
/// </summary> /// </summary>
/// <param name="seconds"></param> /// <param name="seconds"></param>
/// <returns></returns> /// <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> /// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime /// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
@@ -146,13 +146,13 @@ namespace CryptoExchange.Net.Converters
/// </summary> /// </summary>
/// <param name="microseconds"></param> /// <param name="microseconds"></param>
/// <returns></returns> /// <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> /// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
/// <param name="nanoseconds"></param> /// <param name="nanoseconds"></param>
/// <returns></returns> /// <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> /// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value /// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary> /// </summary>
@@ -173,14 +173,14 @@ namespace CryptoExchange.Net.Converters
/// <param name="time"></param> /// <param name="time"></param>
/// <returns></returns> /// <returns></returns>
[return: NotNullIfNotNull("time")] [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> /// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value /// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param> /// <param name="time"></param>
/// <returns></returns> /// <returns></returns>
[return: NotNullIfNotNull("time")] [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 /> /// <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);
}
}
+43 -5
View File
@@ -14,12 +14,29 @@ namespace CryptoExchange.Net.Converters
/// </summary> /// </summary>
public class EnumConverter : JsonConverter 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(); private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ {
return objectType.IsEnum; return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -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"); 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 else
{
// We received an enum value but weren't able to parse it. // We received an enum value but weren't able to parse it.
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"); 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; return defaultValue;
} }
@@ -117,9 +138,12 @@ namespace CryptoExchange.Net.Converters
/// <param name="enumValue"></param> /// <param name="enumValue"></param>
/// <returns></returns> /// <returns></returns>
[return: NotNullIfNotNull("enumValue")] [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; objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
if (!_mapping.TryGetValue(objectType, out var mapping)) if (!_mapping.TryGetValue(objectType, out var mapping))
@@ -131,8 +155,22 @@ namespace CryptoExchange.Net.Converters
/// <inheritdoc /> /// <inheritdoc />
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
var stringValue = GetString(value); if (value == null)
writer.WriteRawValue(stringValue); {
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
};
}
}
+8 -7
View File
@@ -6,18 +6,18 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency API's</Description> <Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>5.2.2</PackageVersion> <PackageVersion>6.2.3</PackageVersion>
<AssemblyVersion>5.2.2</AssemblyVersion> <AssemblyVersion>6.2.3</AssemblyVersion>
<FileVersion>5.2.2</FileVersion> <FileVersion>6.2.3</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl> <RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>5.2.2 - Added support for retrieving a new url when socket connection is lost and reconnection will happen</PackageReleaseNotes> <PackageReleaseNotes>6.2.3 - Fixed requestBodyFormat parameter handling</PackageReleaseNotes>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>9.0</LangVersion> <LangVersion>10.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'"> <PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
@@ -45,10 +45,11 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+65
View File
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System; using System;
using System.Security.Cryptography;
namespace CryptoExchange.Net namespace CryptoExchange.Net
{ {
@@ -8,6 +9,17 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public static class ExchangeHelpers 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> /// <summary>
/// Clamp a value between a min and max /// Clamp a value between a min and max
/// </summary> /// </summary>
@@ -43,7 +55,9 @@ namespace CryptoExchange.Net
var offset = value % step.Value; var offset = value % step.Value;
if(roundingType == RoundingType.Down) if(roundingType == RoundingType.Down)
{
value -= offset; value -= offset;
}
else else
{ {
if (offset < step / 2) if (offset < step / 2)
@@ -116,5 +130,56 @@ namespace CryptoExchange.Net
{ {
return value / 1.000000000000000000000000000000000m; 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>
/// 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);
}
} }
} }
+23 -34
View File
@@ -6,7 +6,6 @@ using System.Runtime.InteropServices;
using System.Security; using System.Security;
using System.Text; using System.Text;
using System.Web; using System.Web;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -90,31 +89,6 @@ namespace CryptoExchange.Net
parameters.Add(key, JsonConvert.SerializeObject(value, converter)); 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> /// <summary>
/// Create a query string of the specified parameters /// Create a query string of the specified parameters
/// </summary> /// </summary>
@@ -129,7 +103,9 @@ namespace CryptoExchange.Net
foreach (var arrayEntry in arraysParameters) foreach (var arrayEntry in arraysParameters)
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&"; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
}
else else
{ {
var array = (Array)arrayEntry.Value; var array = (Array)arrayEntry.Value;
@@ -160,8 +136,10 @@ namespace CryptoExchange.Net
formData.Add(kvp.Key, value.ToString()); formData.Add(kvp.Key, value.ToString());
} }
else else
{
formData.Add(kvp.Key, kvp.Value.ToString()); formData.Add(kvp.Key, kvp.Value.ToString());
} }
}
return formData.ToString(); return formData.ToString();
} }
@@ -224,7 +202,11 @@ namespace CryptoExchange.Net
if (b1 != b2) return false; if (b1 != b2) return false;
} }
} }
else return false; else
{
return false;
}
return true; return true;
} }
finally finally
@@ -252,9 +234,9 @@ namespace CryptoExchange.Net
/// String to JToken /// String to JToken
/// </summary> /// </summary>
/// <param name="stringData"></param> /// <param name="stringData"></param>
/// <param name="log"></param> /// <param name="logger"></param>
/// <returns></returns> /// <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)) if (string.IsNullOrEmpty(stringData))
return null; return null;
@@ -266,15 +248,15 @@ namespace CryptoExchange.Net
catch (JsonReaderException jre) catch (JsonReaderException jre)
{ {
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}"; var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
log?.Write(LogLevel.Error, info); logger?.Log(LogLevel.Error, info);
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}"); if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null; return null;
} }
catch (JsonSerializationException jse) catch (JsonSerializationException jse)
{ {
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}"; var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
log?.Write(LogLevel.Error, info); logger?.Log(LogLevel.Error, info);
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}"); if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
return null; return null;
} }
} }
@@ -288,9 +270,11 @@ namespace CryptoExchange.Net
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues) public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
{ {
if (!allowedValues.Contains(value)) if (!allowedValues.Contains(value))
{
throw new ArgumentException( throw new ArgumentException(
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName); $"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
} }
}
/// <summary> /// <summary>
/// Validates an int is between two values /// Validates an int is between two values
@@ -302,9 +286,11 @@ namespace CryptoExchange.Net
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue) public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
{ {
if (value < minValue || value > maxValue) if (value < minValue || value > maxValue)
{
throw new ArgumentException( throw new ArgumentException(
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName); $"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
} }
}
/// <summary> /// <summary>
/// Validates a string is not null or empty /// Validates a string is not null or empty
@@ -437,8 +423,10 @@ namespace CryptoExchange.Net
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString()); httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
} }
else else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
} }
}
uriBuilder.Query = httpValueCollection.ToString(); uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri; return uriBuilder.Uri;
} }
@@ -466,13 +454,14 @@ namespace CryptoExchange.Net
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString()); httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
} }
else else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString()); httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
} }
}
uriBuilder.Query = httpValueCollection.ToString(); uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri; return uriBuilder.Uri;
} }
/// <summary> /// <summary>
/// Add parameter to URI /// Add parameter to URI
/// </summary> /// </summary>
@@ -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;
}
}
@@ -1,5 +1,5 @@
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System.Net.Http; using System.Net.Http;
using System.Security; using System.Security;
using System.Threading; using System.Threading;
@@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="requestWeight">The weight of the request</param> /// <param name="requestWeight">The weight of the request</param>
/// <param name="ct">Cancellation token to cancel waiting</param> /// <param name="ct">Cancellation token to cancel waiting</param>
/// <returns>The time in milliseconds spend waiting</returns> /// <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);
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System; using System;
using System.Net.Http; using System.Net.Http;
@@ -22,8 +23,8 @@ namespace CryptoExchange.Net.Interfaces
/// Configure the requests created by this factory /// Configure the requests created by this factory
/// </summary> /// </summary>
/// <param name="requestTimeout">Request timeout to use</param> /// <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> /// <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> /// </summary>
bool IsSuccessStatusCode { get; } bool IsSuccessStatusCode { get; }
/// <summary>
/// The length of the response in bytes
/// </summary>
long? ContentLength { get; }
/// <summary> /// <summary>
/// The response headers /// The response headers
/// </summary> /// </summary>
@@ -0,0 +1,21 @@
using CryptoExchange.Net.Objects;
using System;
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; }
}
}
+3 -13
View File
@@ -1,6 +1,7 @@
using System; using System;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
{ {
@@ -10,24 +11,13 @@ namespace CryptoExchange.Net.Interfaces
public interface IRestClient: IDisposable public interface IRestClient: IDisposable
{ {
/// <summary> /// <summary>
/// The factory for creating requests. Used for unit testing /// The options provided for this client
/// </summary> /// </summary>
IRequestFactory RequestFactory { get; set; } ExchangeOptions ClientOptions { get; }
/// <summary> /// <summary>
/// The total amount of requests made with this client /// The total amount of requests made with this client
/// </summary> /// </summary>
int TotalRequestsMade { get; } 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,57 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets;
using System;
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>
/// 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);
}
}
@@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
@@ -14,13 +15,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// The options provided for this client /// The options provided for this client
/// </summary> /// </summary>
BaseSocketClientOptions ClientOptions { get; } ExchangeOptions 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);
/// <summary> /// <summary>
/// Incoming kilobytes per second of data /// Incoming kilobytes per second of data
@@ -101,13 +101,22 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <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 /// 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> /// </summary>
/// <param name="quantity">The quantity in base asset to fill</param> /// <param name="quantity">The quantity in base asset to fill</param>
/// <param name="type">The type</param> /// <param name="type">The type</param>
/// <returns>Average fill price</returns> /// <returns>Average fill price</returns>
CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type); 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> /// <summary>
/// String representation of the top x entries /// String representation of the top x entries
/// </summary> /// </summary>
+9 -3
View File
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
{ {
/// <summary> /// <summary>
/// Webscoket connection interface /// Websocket connection interface
/// </summary> /// </summary>
public interface IWebsocket: IDisposable public interface IWebsocket: IDisposable
{ {
@@ -21,6 +21,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
event Action<string> OnMessage; event Action<string> OnMessage;
/// <summary> /// <summary>
/// Websocket sent event, RequestId as parameter
/// </summary>
event Action<int> OnRequestSent;
/// <summary>
/// Websocket error event /// Websocket error event
/// </summary> /// </summary>
event Action<Exception> OnError; event Action<Exception> OnError;
@@ -39,7 +43,7 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Get reconntion url /// Get reconntion url
/// </summary> /// </summary>
Func<Task<Uri?>> GetReconnectionUrl { get; set; } Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
/// <summary> /// <summary>
/// Unique id for this socket /// Unique id for this socket
@@ -69,8 +73,10 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Send data /// Send data
/// </summary> /// </summary>
/// <param name="id"></param>
/// <param name="data"></param> /// <param name="data"></param>
void Send(string data); /// <param name="weight"></param>
void Send(int id, string data, int weight);
/// <summary> /// <summary>
/// Reconnect the socket /// Reconnect the socket
/// </summary> /// </summary>
@@ -1,5 +1,5 @@
using CryptoExchange.Net.Logging; using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets; using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Interfaces namespace CryptoExchange.Net.Interfaces
{ {
@@ -11,9 +11,9 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Create a websocket for an url /// Create a websocket for an url
/// </summary> /// </summary>
/// <param name="log">The logger</param> /// <param name="logger">The logger</param>
/// <param name="parameters">The parameters to use for the connection</param> /// <param name="parameters">The parameters to use for the connection</param>
/// <returns></returns> /// <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);
}
}
}
-25
View File
@@ -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);
}
}
}
-79
View File
@@ -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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -12,7 +13,7 @@ namespace CryptoExchange.Net.Objects
public class AsyncResetEvent : IDisposable public class AsyncResetEvent : IDisposable
{ {
private static readonly Task<bool> _completed = Task.FromResult(true); 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 bool _signaled;
private readonly bool _reset; private readonly bool _reset;
@@ -48,8 +49,14 @@ namespace CryptoExchange.Net.Objects
{ {
var cancellationSource = new CancellationTokenSource(timeout.Value); var cancellationSource = new CancellationTokenSource(timeout.Value);
var registration = cancellationSource.Token.Register(() => var registration = cancellationSource.Token.Register(() =>
{
lock (_waits)
{ {
tcs.TrySetResult(false); tcs.TrySetResult(false);
// Not the cleanest but it works
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
}
}, useSynchronizationContext: false); }, useSynchronizationContext: false);
} }
+95 -22
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
{ {
@@ -38,6 +39,12 @@ namespace CryptoExchange.Net.Objects
{ {
return obj?.Success == true; return obj?.Success == true;
} }
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
} }
/// <summary> /// <summary>
@@ -128,6 +135,24 @@ namespace CryptoExchange.Net.Objects
return new CallResult<K>(data, OriginalData, Error); 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> /// <summary>
/// Copy the WebCallResult to a new data type /// Copy the WebCallResult to a new data type
/// </summary> /// </summary>
@@ -138,6 +163,12 @@ namespace CryptoExchange.Net.Objects
{ {
return new CallResult<K>(default, OriginalData, error); return new CallResult<K>(default, OriginalData, error);
} }
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
} }
/// <summary> /// <summary>
@@ -155,6 +186,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; } public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary> /// <summary>
/// The url which was requested /// The url which was requested
/// </summary> /// </summary>
@@ -186,6 +222,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="code"></param> /// <param name="code"></param>
/// <param name="responseHeaders"></param> /// <param name="responseHeaders"></param>
/// <param name="responseTime"></param> /// <param name="responseTime"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param> /// <param name="requestUrl"></param>
/// <param name="requestBody"></param> /// <param name="requestBody"></param>
/// <param name="requestMethod"></param> /// <param name="requestMethod"></param>
@@ -195,6 +232,7 @@ namespace CryptoExchange.Net.Objects
HttpStatusCode? code, HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
TimeSpan? responseTime, TimeSpan? responseTime,
int? requestId,
string? requestUrl, string? requestUrl,
string? requestBody, string? requestBody,
HttpMethod? requestMethod, HttpMethod? requestMethod,
@@ -204,6 +242,7 @@ namespace CryptoExchange.Net.Objects
ResponseStatusCode = code; ResponseStatusCode = code;
ResponseHeaders = responseHeaders; ResponseHeaders = responseHeaders;
ResponseTime = responseTime; ResponseTime = responseTime;
RequestId = requestId;
RequestUrl = requestUrl; RequestUrl = requestUrl;
RequestBody = requestBody; RequestBody = requestBody;
@@ -224,7 +263,13 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public WebCallResult AsError(Error error) 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 +289,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; } public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary> /// <summary>
/// The url which was requested /// The url which was requested
/// </summary> /// </summary>
@@ -259,6 +309,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public HttpStatusCode? ResponseStatusCode { get; set; } public HttpStatusCode? ResponseStatusCode { get; set; }
/// <summary>
/// Length in bytes of the response
/// </summary>
public long? ResponseLength { get; set; }
/// <summary> /// <summary>
/// The response headers /// The response headers
/// </summary> /// </summary>
@@ -275,7 +330,9 @@ namespace CryptoExchange.Net.Objects
/// <param name="code"></param> /// <param name="code"></param>
/// <param name="responseHeaders"></param> /// <param name="responseHeaders"></param>
/// <param name="responseTime"></param> /// <param name="responseTime"></param>
/// <param name="responseLength"></param>
/// <param name="originalData"></param> /// <param name="originalData"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param> /// <param name="requestUrl"></param>
/// <param name="requestBody"></param> /// <param name="requestBody"></param>
/// <param name="requestMethod"></param> /// <param name="requestMethod"></param>
@@ -286,7 +343,9 @@ namespace CryptoExchange.Net.Objects
HttpStatusCode? code, HttpStatusCode? code,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
TimeSpan? responseTime, TimeSpan? responseTime,
long? responseLength,
string? originalData, string? originalData,
int? requestId,
string? requestUrl, string? requestUrl,
string? requestBody, string? requestBody,
HttpMethod? requestMethod, HttpMethod? requestMethod,
@@ -297,18 +356,37 @@ namespace CryptoExchange.Net.Objects
ResponseStatusCode = code; ResponseStatusCode = code;
ResponseHeaders = responseHeaders; ResponseHeaders = responseHeaders;
ResponseTime = responseTime; ResponseTime = responseTime;
ResponseLength = responseLength;
RequestId = requestId;
RequestUrl = requestUrl; RequestUrl = requestUrl;
RequestBody = requestBody; RequestBody = requestBody;
RequestHeaders = requestHeaders; RequestHeaders = requestHeaders;
RequestMethod = requestMethod; 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> /// <summary>
/// Create a new error result /// Create a new error result
/// </summary> /// </summary>
/// <param name="error">The error</param> /// <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> /// <summary>
/// Copy the WebCallResult to a new data type /// Copy the WebCallResult to a new data type
@@ -318,25 +396,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data) public new WebCallResult<K> As<K>([AllowNull] K data)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error); return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, 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);
} }
/// <summary> /// <summary>
@@ -347,7 +407,20 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error) 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();
} }
} }
} }
+15
View File
@@ -124,4 +124,19 @@
/// </summary> /// </summary>
Closest 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
}
} }
+127 -8
View File
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.Objects using System;
namespace CryptoExchange.Net.Objects
{ {
/// <summary> /// <summary>
/// Base class for errors /// Base class for errors
@@ -39,7 +41,7 @@
/// <returns></returns> /// <returns></returns>
public override string ToString() 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 /// ctor
/// </summary> /// </summary>
public CantConnectError() : base(null, "Can't connect to the server", null) { } 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> /// <summary>
@@ -63,6 +73,14 @@
/// ctor /// ctor
/// </summary> /// </summary>
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { } 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> /// <summary>
@@ -83,9 +101,15 @@
/// <param name="code"></param> /// <param name="code"></param>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="data"></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> /// <summary>
@@ -107,6 +131,14 @@
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="data"></param> /// <param name="data"></param>
public WebError(int code, string message, object? data = null) : base(code, message, data) { } 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> /// <summary>
@@ -120,6 +152,14 @@
/// <param name="message">The error message</param> /// <param name="message">The error message</param>
/// <param name="data">The data which caused the error</param> /// <param name="data">The data which caused the error</param>
public DeserializeError(string message, object? data) : base(null, message, data) { } 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> /// <summary>
@@ -133,6 +173,14 @@
/// <param name="message">Error message</param> /// <param name="message">Error message</param>
/// <param name="data">Error data</param> /// <param name="data">Error data</param>
public UnknownError(string message, object? data = null) : base(null, message, data) { } 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> /// <summary>
@@ -145,18 +193,73 @@
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { } 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> /// <summary>
/// Rate limit exceeded /// Rate limit exceeded (client side)
/// </summary> /// </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> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="message"></param> /// <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> /// <summary>
@@ -168,6 +271,14 @@
/// ctor /// ctor
/// </summary> /// </summary>
public CancellationRequestedError() : base(null, "Cancellation requested", null) { } public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
/// <summary>
/// ctor
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
} }
/// <summary> /// <summary>
@@ -180,5 +291,13 @@
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
public InvalidOperationError(string message) : base(null, message, null) { } 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) { }
} }
} }
-361
View File
@@ -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));
}
}
}
+72 -37
View File
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -18,7 +17,7 @@ namespace CryptoExchange.Net.Objects
public class RateLimiter : IRateLimiter public class RateLimiter : IRateLimiter
{ {
private readonly object _limiterLock = new object(); private readonly object _limiterLock = new object();
internal List<Limiter> Limiters = new List<Limiter>(); internal List<Limiter> _limiters = new List<Limiter>();
/// <summary> /// <summary>
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>, /// 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) public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null)); _limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
return this; 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) public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits)); _limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this; 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) public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
{ {
lock(_limiterLock) lock(_limiterLock)
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits)); _limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
return this; 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) public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
{ {
lock(_limiterLock) 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; return this;
} }
@@ -96,33 +95,46 @@ namespace CryptoExchange.Net.Objects
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit) public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
{ {
lock(_limiterLock) 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; return this;
} }
/// <inheritdoc /> /// <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; int totalWaitTime = 0;
EndpointRateLimiter? endpointLimit; List<EndpointRateLimiter> endpointLimits;
lock (_limiterLock) lock (_limiterLock)
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)); endpointLimits = _limiters.OfType<EndpointRateLimiter>().Where(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method)).ToList();
if(endpointLimit != null) 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) if (!waitResult)
return waitResult; return waitResult;
totalWaitTime += waitResult.Data; totalWaitTime += waitResult.Data;
} }
if (endpointLimit?.IgnoreOtherRateLimits == true) if (endpointLimits.Any(l => l.IgnoreOtherRateLimits))
return new CallResult<int>(totalWaitTime); return new CallResult<int>(totalWaitTime);
List<PartialEndpointRateLimiter> partialEndpointLimits; List<PartialEndpointRateLimiter> partialEndpointLimits;
lock (_limiterLock) 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) foreach (var partialEndpointLimit in partialEndpointLimits)
{ {
if (partialEndpointLimit.CountPerEndpoint) if (partialEndpointLimit.CountPerEndpoint)
@@ -130,15 +142,15 @@ namespace CryptoExchange.Net.Objects
SingleTopicRateLimiter? thisEndpointLimit; SingleTopicRateLimiter? thisEndpointLimit;
lock (_limiterLock) 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) if (thisEndpointLimit == null)
{ {
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit); 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) if (!waitResult)
return waitResult; return waitResult;
@@ -146,7 +158,7 @@ namespace CryptoExchange.Net.Objects
} }
else 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) if (!waitResult)
return waitResult; return waitResult;
@@ -157,16 +169,16 @@ namespace CryptoExchange.Net.Objects
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits)) if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
return new CallResult<int>(totalWaitTime); return new CallResult<int>(totalWaitTime);
ApiKeyRateLimiter? apiLimit; List<ApiKeyRateLimiter> apiLimits;
lock (_limiterLock) lock (_limiterLock)
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey); apiLimits = _limiters.OfType<ApiKeyRateLimiter>().Where(h => h.Type == RateLimitType.ApiKey).ToList();
if (apiLimit != null) foreach (var apiLimit in apiLimits)
{ {
if(apiKey == null) if(apiKey == null)
{ {
if (!apiLimit.OnlyForSignedRequests) 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) if (!waitResult)
return waitResult; return waitResult;
@@ -178,15 +190,15 @@ namespace CryptoExchange.Net.Objects
SingleTopicRateLimiter? thisApiLimit; SingleTopicRateLimiter? thisApiLimit;
lock (_limiterLock) 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) if (thisApiLimit == null)
{ {
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit); 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) if (!waitResult)
return 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); return new CallResult<int>(totalWaitTime);
TotalRateLimiter? totalLimit; List<TotalRateLimiter> totalLimits;
lock (_limiterLock) lock (_limiterLock)
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault(); totalLimits = _limiters.OfType<TotalRateLimiter>().ToList();
if (totalLimit != null) 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) if (!waitResult)
return waitResult; return waitResult;
@@ -212,7 +224,7 @@ namespace CryptoExchange.Net.Objects
return new CallResult<int>(totalWaitTime); 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(); var sw = Stopwatch.StartNew();
try try
@@ -225,6 +237,8 @@ namespace CryptoExchange.Net.Objects
} }
sw.Stop(); sw.Stop();
try
{
int totalWaitTime = 0; int totalWaitTime = 0;
while (true) while (true)
{ {
@@ -249,18 +263,17 @@ namespace CryptoExchange.Net.Objects
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}"); $"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 // Wait until the next entry should be removed from the history
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds); var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
if (thisWaitTime > 0) if (thisWaitTime > 0)
{ {
if (limitBehaviour == RateLimitingBehaviour.Fail) 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}"; 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); logger.Log(LogLevel.Warning, msg);
return new CallResult<int>(new RateLimitError(msg)); return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
} }
log.Write(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}"); logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
try try
{ {
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false); await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
@@ -280,9 +293,13 @@ namespace CryptoExchange.Net.Objects
var newTime = DateTime.UtcNow; var newTime = DateTime.UtcNow;
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight)); historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
historyTopic.Semaphore.Release();
return new CallResult<int>(totalWaitTime); return new CallResult<int>(totalWaitTime);
} }
finally
{
historyTopic.Semaphore.Release();
}
}
internal struct LimitEntry 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 internal class EndpointRateLimiter: Limiter
{ {
public string[] Endpoints { get; set; } public string[] Endpoints { get; set; }
+6 -7
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.Threading; using System.Threading;
using CryptoExchange.Net.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
@@ -45,7 +44,7 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// Logger /// Logger
/// </summary> /// </summary>
public Log Log { get; } public ILogger Logger { get; }
/// <summary> /// <summary>
/// Should synchronize time /// Should synchronize time
/// </summary> /// </summary>
@@ -62,13 +61,13 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="log"></param> /// <param name="logger"></param>
/// <param name="recalculationInterval"></param> /// <param name="recalculationInterval"></param>
/// <param name="syncTime"></param> /// <param name="syncTime"></param>
/// <param name="syncState"></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; SyncTime = syncTime;
RecalculationInterval = recalculationInterval; RecalculationInterval = recalculationInterval;
TimeSyncState = syncState; TimeSyncState = syncState;
@@ -83,12 +82,12 @@ namespace CryptoExchange.Net.Objects
TimeSyncState.LastSyncTime = DateTime.UtcNow; TimeSyncState.LastSyncTime = DateTime.UtcNow;
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) 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; TimeSyncState.TimeOffset = TimeSpan.Zero;
} }
else 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; TimeSyncState.TimeOffset = offset;
} }
} }
+63
View File
@@ -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,39 @@
using System.Collections.Generic;
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 /// First sequence number in this update
/// </summary> /// </summary>
public long FirstUpdateId { get; set; } public long FirstUpdateId { get; set; }
/// <summary> /// <summary>
/// Last sequence number in this update /// Last sequence number in this update
/// </summary> /// </summary>
public long LastUpdateId { get; set; } public long LastUpdateId { get; set; }
/// <summary> /// <summary>
/// List of changed/new asks /// List of changed/new asks
/// </summary> /// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>(); public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
/// <summary> /// <summary>
/// List of changed/new bids /// List of changed/new bids
/// </summary> /// </summary>
+116 -77
View File
@@ -7,8 +7,8 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -30,7 +30,7 @@ namespace CryptoExchange.Net.OrderBook
private readonly AsyncResetEvent _queueEvent; private readonly AsyncResetEvent _queueEvent;
private readonly ConcurrentQueue<object> _processQueue; private readonly ConcurrentQueue<object> _processQueue;
private readonly bool _validateChecksum; private bool _validateChecksum;
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
{ {
@@ -40,48 +40,47 @@ namespace CryptoExchange.Net.OrderBook
set { } } set { } }
} }
private static readonly ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry(); private static readonly ISymbolOrderBookEntry _emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
/// <summary> /// <summary>
/// A buffer to store messages received before the initial book snapshot is processed. These messages /// 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 /// 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 /// than the snapshot sequence number will be discarded
/// </summary> /// </summary>
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer; protected readonly List<ProcessBufferRangeSequenceEntry> _processBuffer;
/// <summary> /// <summary>
/// The ask list, should only be accessed using the bookLock /// The ask list, should only be accessed using the bookLock
/// </summary> /// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> asks; protected SortedList<decimal, ISymbolOrderBookEntry> _asks;
/// <summary> /// <summary>
/// The bid list, should only be accessed using the bookLock /// The bid list, should only be accessed using the bookLock
/// </summary> /// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> bids; protected SortedList<decimal, ISymbolOrderBookEntry> _bids;
/// <summary> /// <summary>
/// The log /// The log
/// </summary> /// </summary>
protected Log log; protected ILogger _logger;
/// <summary> /// <summary>
/// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1 /// 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 /// the book will resynchronize as it is deemed out of sync
/// </summary> /// </summary>
protected bool sequencesAreConsecutive; protected bool _sequencesAreConsecutive;
/// <summary> /// <summary>
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes /// 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 /// 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 /// server handle this
/// </summary> /// </summary>
protected bool strictLevels; protected bool _strictLevels;
/// <summary> /// <summary>
/// If the initial snapshot of the book has been set /// If the initial snapshot of the book has been set
/// </summary> /// </summary>
protected bool bookSet; protected bool _bookSet;
/// <summary> /// <summary>
/// The amount of levels for this book /// The amount of levels for this book
@@ -102,7 +101,7 @@ namespace CryptoExchange.Net.OrderBook
var old = _status; var old = _status;
_status = value; _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); OnStatusChange?.Invoke(old, _status);
} }
} }
@@ -137,7 +136,7 @@ namespace CryptoExchange.Net.OrderBook
get get
{ {
lock (_bookLock) 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 get
{ {
lock (_bookLock) 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 get
{ {
lock (_bookLock) lock (_bookLock)
return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry; return _bids.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
} }
} }
@@ -177,7 +176,7 @@ namespace CryptoExchange.Net.OrderBook
get get
{ {
lock (_bookLock) lock (_bookLock)
return asks.FirstOrDefault().Value ?? emptySymbolOrderBookEntry; return _asks.FirstOrDefault().Value ?? _emptySymbolOrderBookEntry;
} }
} }
@@ -192,32 +191,39 @@ namespace CryptoExchange.Net.OrderBook
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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="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="symbol">The symbol the order book is for</param>
/// <param name="options">The options for the order book</param> protected SymbolOrderBook(ILogger? logger, string id, string symbol)
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
{ {
if (symbol == null) if (symbol == null)
throw new ArgumentNullException(nameof(symbol)); throw new ArgumentNullException(nameof(symbol));
if (options == null)
throw new ArgumentNullException(nameof(options));
Id = id; Id = id;
processBuffer = new List<ProcessBufferRangeSequenceEntry>(); _processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processQueue = new ConcurrentQueue<object>(); _processQueue = new ConcurrentQueue<object>();
_queueEvent = new AsyncResetEvent(false, true); _queueEvent = new AsyncResetEvent(false, true);
_validateChecksum = options.ChecksumValidationEnabled;
Symbol = symbol; Symbol = symbol;
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
asks = new SortedList<decimal, ISymbolOrderBookEntry>(); _asks = new SortedList<decimal, ISymbolOrderBookEntry>();
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>()); _bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
log = new Log(id) { Level = options.LogLevel }; _logger = logger ?? new TraceLogger();
var writers = options.LogWriters ?? new List<ILogger> { new DebugLogger() }; }
log.UpdateWriters(writers.ToList());
/// <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/> /// <inheritdoc/>
@@ -226,7 +232,7 @@ namespace CryptoExchange.Net.OrderBook
if (Status != OrderBookStatus.Disconnected) if (Status != OrderBookStatus.Disconnected)
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}"); 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(); _cts = new CancellationTokenSource();
ct?.Register(async () => ct?.Register(async () =>
{ {
@@ -236,8 +242,8 @@ namespace CryptoExchange.Net.OrderBook
// Clear any previous messages // Clear any previous messages
while (_processQueue.TryDequeue(out _)) { } while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear(); _processBuffer.Clear();
bookSet = false; _bookSet = false;
Status = OrderBookStatus.Connecting; Status = OrderBookStatus.Connecting;
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning); _processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
@@ -251,7 +257,7 @@ namespace CryptoExchange.Net.OrderBook
if (_cts.IsCancellationRequested) 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); await startResult.Data.CloseAsync().ConfigureAwait(false);
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(new CancellationRequestedError()); return new CallResult<bool>(new CancellationRequestedError());
@@ -267,7 +273,7 @@ namespace CryptoExchange.Net.OrderBook
} }
private void HandleConnectionLost() { 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) { if (Status != OrderBookStatus.Disposed) {
Status = OrderBookStatus.Reconnecting; Status = OrderBookStatus.Reconnecting;
Reset(); Reset();
@@ -275,7 +281,7 @@ namespace CryptoExchange.Net.OrderBook
} }
private void HandleConnectionClosed() { private void HandleConnectionClosed() {
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected"); _logger.Log(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
_ = StopAsync(); _ = StopAsync();
} }
@@ -287,7 +293,7 @@ namespace CryptoExchange.Net.OrderBook
/// <inheritdoc/> /// <inheritdoc/>
public async Task StopAsync() 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; Status = OrderBookStatus.Disconnected;
_cts?.Cancel(); _cts?.Cancel();
_queueEvent.Set(); _queueEvent.Set();
@@ -300,21 +306,21 @@ namespace CryptoExchange.Net.OrderBook
_subscription.ConnectionClosed -= HandleConnectionClosed; _subscription.ConnectionClosed -= HandleConnectionClosed;
_subscription.ConnectionRestored -= HandleConnectionRestored; _subscription.ConnectionRestored -= HandleConnectionRestored;
} }
log.Write(LogLevel.Trace, $"{Id} order book {Symbol} stopped"); _logger.Log(LogLevel.Trace, $"{Id} order book {Symbol} stopped");
} }
/// <inheritdoc/> /// <inheritdoc/>
public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type) public CallResult<decimal> CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type)
{ {
if (Status != OrderBookStatus.Synced) if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
var totalCost = 0m; var totalCost = 0m;
var totalAmount = 0m; var totalAmount = 0m;
var amountLeft = quantity; var amountLeft = baseQuantity;
lock (_bookLock) lock (_bookLock)
{ {
var list = type == OrderBookEntryType.Ask ? asks : bids; var list = type == OrderBookEntryType.Ask ? _asks : _bids;
var step = 0; var step = 0;
while (amountLeft > 0) while (amountLeft > 0)
@@ -334,6 +340,35 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8)); 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> /// <summary>
/// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving /// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving
/// and setting the initial order book /// and setting the initial order book
@@ -427,14 +462,14 @@ namespace CryptoExchange.Net.OrderBook
/// </summary> /// </summary>
protected void CheckProcessBuffer() protected void CheckProcessBuffer()
{ {
var pbList = processBuffer.ToList(); var pbList = _processBuffer.ToList();
if (pbList.Count > 0) 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) foreach (var bufferEntry in pbList)
{ {
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks); 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) 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; return false;
} }
if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1) if (_sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
{ {
// Out of sync // 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; _stopProcessing = true;
Resubscribe(); Resubscribe();
return false; return false;
} }
UpdateTime = DateTime.UtcNow; UpdateTime = DateTime.UtcNow;
var listToChange = type == OrderBookEntryType.Ask ? asks : bids; var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
if (entry.Quantity == 0) if (entry.Quantity == 0)
{ {
if (!listToChange.ContainsKey(entry.Price)) if (!listToChange.ContainsKey(entry.Price))
@@ -498,7 +533,7 @@ namespace CryptoExchange.Net.OrderBook
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct) protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
{ {
var startWait = DateTime.UtcNow; var startWait = DateTime.UtcNow;
while (!bookSet && Status == OrderBookStatus.Syncing) while (!_bookSet && Status == OrderBookStatus.Syncing)
{ {
if(ct.IsCancellationRequested) if(ct.IsCancellationRequested)
return new CallResult<bool>(new CancellationRequestedError()); return new CallResult<bool>(new CancellationRequestedError());
@@ -540,9 +575,9 @@ namespace CryptoExchange.Net.OrderBook
// Clear queue // Clear queue
while (_processQueue.TryDequeue(out _)) { } while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear(); _processBuffer.Clear();
asks.Clear(); _asks.Clear();
bids.Clear(); _bids.Clear();
AskCount = 0; AskCount = 0;
BidCount = 0; BidCount = 0;
@@ -581,16 +616,18 @@ namespace CryptoExchange.Net.OrderBook
var (bestBid, bestAsk) = BestOffers; var (bestBid, bestAsk) = BestOffers;
if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity || if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity ||
bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity) bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity)
{
OnBestOffersChanged?.Invoke((bestBid, bestAsk)); OnBestOffersChanged?.Invoke((bestBid, bestAsk));
} }
}
private void Reset() private void Reset()
{ {
_queueEvent.Set(); _queueEvent.Set();
// Clear queue // Clear queue
while (_processQueue.TryDequeue(out _)) { } while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear(); _processBuffer.Clear();
bookSet = false; _bookSet = false;
DoReset(); DoReset();
} }
@@ -607,7 +644,7 @@ namespace CryptoExchange.Net.OrderBook
success = resyncResult; 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; Status = OrderBookStatus.Synced;
} }
@@ -624,7 +661,7 @@ namespace CryptoExchange.Net.OrderBook
if (_stopProcessing) if (_stopProcessing)
{ {
log.Write(LogLevel.Trace, "Skipping message because of resubscribing"); _logger.Log(LogLevel.Trace, $"{Id} Skipping message because of resubscribing");
continue; continue;
} }
@@ -642,21 +679,21 @@ namespace CryptoExchange.Net.OrderBook
{ {
lock (_bookLock) lock (_bookLock)
{ {
bookSet = true; _bookSet = true;
asks.Clear(); _asks.Clear();
foreach (var ask in item.Asks) foreach (var ask in item.Asks)
asks.Add(ask.Price, ask); _asks.Add(ask.Price, ask);
bids.Clear(); _bids.Clear();
foreach (var bid in item.Bids) foreach (var bid in item.Bids)
bids.Add(bid.Price, bid); _bids.Add(bid.Price, bid);
LastSequenceNumber = item.EndUpdateId; LastSequenceNumber = item.EndUpdateId;
AskCount = asks.Count; AskCount = _asks.Count;
BidCount = bids.Count; BidCount = _bids.Count;
UpdateTime = DateTime.UtcNow; 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(); CheckProcessBuffer();
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks)); OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
OnBestOffersChanged?.Invoke((BestBid, BestAsk)); OnBestOffersChanged?.Invoke((BestBid, BestAsk));
@@ -667,16 +704,16 @@ namespace CryptoExchange.Net.OrderBook
{ {
lock (_bookLock) lock (_bookLock)
{ {
if (!bookSet) if (!_bookSet)
{ {
processBuffer.Add(new ProcessBufferRangeSequenceEntry() _processBuffer.Add(new ProcessBufferRangeSequenceEntry()
{ {
Asks = item.Asks, Asks = item.Asks,
Bids = item.Bids, Bids = item.Bids,
FirstUpdateId = item.StartUpdateId, FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId, 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 else
{ {
@@ -684,12 +721,12 @@ namespace CryptoExchange.Net.OrderBook
var (prevBestBid, prevBestAsk) = BestOffers; var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks); ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
if (!asks.Any() || !bids.Any()) if (!_asks.Any() || !_bids.Any())
return; 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; _stopProcessing = true;
Resubscribe(); Resubscribe();
return; return;
@@ -723,7 +760,7 @@ namespace CryptoExchange.Net.OrderBook
if (!checksumResult) 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; _stopProcessing = true;
Resubscribe(); Resubscribe();
} }
@@ -747,12 +784,14 @@ namespace CryptoExchange.Net.OrderBook
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false)) if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
{ {
// Resubscribing failed, reconnect the socket // 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; Status = OrderBookStatus.Reconnecting;
_ = _subscription!.ReconnectAsync(); _ = _subscription!.ReconnectAsync();
} }
else else
{
await ResyncAsync().ConfigureAwait(false); await ResyncAsync().ConfigureAwait(false);
}
}); });
} }
@@ -760,7 +799,7 @@ namespace CryptoExchange.Net.OrderBook
{ {
if (lastUpdateId <= LastSequenceNumber) 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; return;
} }
@@ -770,23 +809,23 @@ namespace CryptoExchange.Net.OrderBook
foreach (var entry in asks) foreach (var entry in asks)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry); 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--; 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--; AskCount--;
this.asks.Remove(this.asks.Last().Key); this._asks.Remove(this._asks.Last().Key);
} }
} }
LastSequenceNumber = lastUpdateId; 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}");
} }
} }
+13 -13
View File
@@ -15,8 +15,8 @@ namespace CryptoExchange.Net.Requests
/// </summary> /// </summary>
public class Request : IRequest public class Request : IRequest
{ {
private readonly HttpRequestMessage request; private readonly HttpRequestMessage _request;
private readonly HttpClient httpClient; private readonly HttpClient _httpClient;
/// <summary> /// <summary>
/// Create request object for web request /// Create request object for web request
@@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Requests
/// <param name="requestId"></param> /// <param name="requestId"></param>
public Request(HttpRequestMessage request, HttpClient client, int requestId) public Request(HttpRequestMessage request, HttpClient client, int requestId)
{ {
httpClient = client; _httpClient = client;
this.request = request; _request = request;
RequestId = requestId; RequestId = requestId;
} }
@@ -37,18 +37,18 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc /> /// <inheritdoc />
public string Accept public string Accept
{ {
set => request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value)); set => _request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(value));
} }
/// <inheritdoc /> /// <inheritdoc />
public HttpMethod Method public HttpMethod Method
{ {
get => request.Method; get => _request.Method;
set => request.Method = value; set => _request.Method = value;
} }
/// <inheritdoc /> /// <inheritdoc />
public Uri Uri => request.RequestUri; public Uri Uri => _request.RequestUri;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
@@ -57,31 +57,31 @@ namespace CryptoExchange.Net.Requests
public void SetContent(string data, string contentType) public void SetContent(string data, string contentType)
{ {
Content = data; Content = data;
request.Content = new StringContent(data, Encoding.UTF8, contentType); _request.Content = new StringContent(data, Encoding.UTF8, contentType);
} }
/// <inheritdoc /> /// <inheritdoc />
public void AddHeader(string key, string value) public void AddHeader(string key, string value)
{ {
request.Headers.Add(key, value); _request.Headers.Add(key, value);
} }
/// <inheritdoc /> /// <inheritdoc />
public Dictionary<string, IEnumerable<string>> GetHeaders() 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 /> /// <inheritdoc />
public void SetContent(byte[] data) public void SetContent(byte[] data)
{ {
request.Content = new ByteArrayContent(data); _request.Content = new ByteArrayContent(data);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken) 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));
} }
} }
} }
+15 -11
View File
@@ -1,8 +1,10 @@
using System; using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Requests namespace CryptoExchange.Net.Requests
{ {
@@ -11,37 +13,39 @@ namespace CryptoExchange.Net.Requests
/// </summary> /// </summary>
public class RequestFactory : IRequestFactory public class RequestFactory : IRequestFactory
{ {
private HttpClient? httpClient; private HttpClient? _httpClient;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? client = null) public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
{ {
if (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}"), Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password) Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
}
}; };
}
httpClient = new HttpClient(handler) { Timeout = requestTimeout }; client = new HttpClient(handler)
}
else
{ {
httpClient = client; Timeout = requestTimeout
};
} }
_httpClient = client;
} }
/// <inheritdoc /> /// <inheritdoc />
public IRequest Create(HttpMethod method, Uri uri, int requestId) 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"); 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);
} }
} }
} }
+10 -7
View File
@@ -12,16 +12,19 @@ namespace CryptoExchange.Net.Requests
/// </summary> /// </summary>
internal class Response : IResponse internal class Response : IResponse
{ {
private readonly HttpResponseMessage response; private readonly HttpResponseMessage _response;
/// <inheritdoc /> /// <inheritdoc />
public HttpStatusCode StatusCode => response.StatusCode; public HttpStatusCode StatusCode => _response.StatusCode;
/// <inheritdoc /> /// <inheritdoc />
public bool IsSuccessStatusCode => response.IsSuccessStatusCode; public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
/// <inheritdoc /> /// <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> /// <summary>
/// Create response for a http response message /// Create response for a http response message
@@ -29,19 +32,19 @@ namespace CryptoExchange.Net.Requests
/// <param name="response">The actual response</param> /// <param name="response">The actual response</param>
public Response(HttpResponseMessage response) public Response(HttpResponseMessage response)
{ {
this.response = response; this._response = response;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<Stream> GetResponseStreamAsync() public async Task<Stream> GetResponseStreamAsync()
{ {
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); return await _response.Content.ReadAsStreamAsync().ConfigureAwait(false);
} }
/// <inheritdoc /> /// <inheritdoc />
public void Close() public void Close()
{ {
response.Dispose(); _response.Dispose();
} }
} }
} }
@@ -1,5 +1,4 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
@@ -8,6 +7,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -27,13 +27,12 @@ namespace CryptoExchange.Net.Sockets
Reconnecting Reconnecting
} }
internal static int lastStreamId; internal static int _lastStreamId;
private static readonly object streamIdLock = new(); private static readonly object _streamIdLock = new();
private readonly AsyncResetEvent _sendEvent; private readonly AsyncResetEvent _sendEvent;
private readonly ConcurrentQueue<byte[]> _sendBuffer; private readonly ConcurrentQueue<SendItem> _sendBuffer;
private readonly SemaphoreSlim _closeSem; private readonly SemaphoreSlim _closeSem;
private readonly List<DateTime> _outgoingMessages;
private ClientWebSocket _socket; private ClientWebSocket _socket;
private CancellationTokenSource _ctsSource; private CancellationTokenSource _ctsSource;
@@ -43,6 +42,7 @@ namespace CryptoExchange.Net.Sockets
private bool _stopRequested; private bool _stopRequested;
private bool _disposed; private bool _disposed;
private ProcessState _processState; private ProcessState _processState;
private DateTime _lastReconnectTime;
/// <summary> /// <summary>
@@ -58,7 +58,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Log /// Log
/// </summary> /// </summary>
protected Log _log; protected ILogger _logger;
/// <inheritdoc /> /// <inheritdoc />
public int Id { get; } public int Id { get; }
@@ -99,14 +99,22 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc /> /// <inheritdoc />
public event Action? OnClose; public event Action? OnClose;
/// <inheritdoc /> /// <inheritdoc />
public event Action<string>? OnMessage; public event Action<string>? OnMessage;
/// <inheritdoc />
public event Action<int>? OnRequestSent;
/// <inheritdoc /> /// <inheritdoc />
public event Action<Exception>? OnError; public event Action<Exception>? OnError;
/// <inheritdoc /> /// <inheritdoc />
public event Action? OnOpen; public event Action? OnOpen;
/// <inheritdoc /> /// <inheritdoc />
public event Action? OnReconnecting; public event Action? OnReconnecting;
/// <inheritdoc /> /// <inheritdoc />
public event Action? OnReconnected; public event Action? OnReconnected;
/// <inheritdoc /> /// <inheritdoc />
@@ -115,18 +123,17 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </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> /// <param name="websocketParameters">The parameters for this socket</param>
public CryptoExchangeWebSocketClient(Log log, WebSocketParameters websocketParameters) public CryptoExchangeWebSocketClient(ILogger logger, WebSocketParameters websocketParameters)
{ {
Id = NextStreamId(); Id = NextStreamId();
_log = log; _logger = logger;
Parameters = websocketParameters; Parameters = websocketParameters;
_outgoingMessages = new List<DateTime>();
_receivedMessages = new List<ReceiveItem>(); _receivedMessages = new List<ReceiveItem>();
_sendEvent = new AsyncResetEvent(); _sendEvent = new AsyncResetEvent();
_sendBuffer = new ConcurrentQueue<byte[]>(); _sendBuffer = new ConcurrentQueue<SendItem>();
_ctsSource = new CancellationTokenSource(); _ctsSource = new CancellationTokenSource();
_receivedMessagesLock = new object(); _receivedMessagesLock = new object();
@@ -155,19 +162,28 @@ namespace CryptoExchange.Net.Sockets
cookieContainer.Add(new Cookie(cookie.Key, cookie.Value)); cookieContainer.Add(new Cookie(cookie.Key, cookie.Value));
var socket = new ClientWebSocket(); var socket = new ClientWebSocket();
try
{
socket.Options.Cookies = cookieContainer; socket.Options.Cookies = cookieContainer;
foreach (var header in Parameters.Headers) foreach (var header in Parameters.Headers)
socket.Options.SetRequestHeader(header.Key, header.Value); socket.Options.SetRequestHeader(header.Key, header.Value);
socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero; 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 socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework
if (Parameters.Proxy != null) if (Parameters.Proxy != null)
SetProxy(Parameters.Proxy); 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; return socket;
} }
private async Task<bool> ConnectInternalAsync() private async Task<bool> ConnectInternalAsync()
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} connecting"); _logger.Log(LogLevel.Debug, $"Socket {Id} connecting");
try try
{ {
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10)); using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
@@ -175,11 +191,11 @@ namespace CryptoExchange.Net.Sockets
} }
catch (Exception e) catch (Exception e)
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString()); _logger.Log(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
return false; return false;
} }
_log.Write(LogLevel.Debug, $"Socket {Id} connected to {Uri}"); _logger.Log(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
return true; return true;
} }
@@ -188,13 +204,13 @@ namespace CryptoExchange.Net.Sockets
{ {
while (!_stopRequested) while (!_stopRequested)
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} starting processing tasks"); _logger.Log(LogLevel.Debug, $"Socket {Id} starting processing tasks");
_processState = ProcessState.Processing; _processState = ProcessState.Processing;
var sendTask = SendLoopAsync(); var sendTask = SendLoopAsync();
var receiveTask = ReceiveLoopAsync(); 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); await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
_log.Write(LogLevel.Debug, $"Socket {Id} processing tasks finished"); _logger.Log(LogLevel.Debug, $"Socket {Id} processing tasks finished");
_processState = ProcessState.WaitingForClose; _processState = ProcessState.WaitingForClose;
while (_closeTask == null) while (_closeTask == null)
@@ -216,16 +232,20 @@ namespace CryptoExchange.Net.Sockets
OnReconnecting?.Invoke(); OnReconnecting?.Invoke();
} }
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
if (sinceLastReconnect < Parameters.ReconnectInterval)
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
while (!_stopRequested) while (!_stopRequested)
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} attempting to reconnect"); _logger.Log(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
var task = GetReconnectionUrl?.Invoke(); var task = GetReconnectionUrl?.Invoke();
if (task != null) if (task != null)
{ {
var reconnectUri = await task.ConfigureAwait(false); var reconnectUri = await task.ConfigureAwait(false);
if (reconnectUri != null && Parameters.Uri != reconnectUri) if (reconnectUri != null && Parameters.Uri != reconnectUri)
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}"); _logger.Log(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
Parameters.Uri = reconnectUri; Parameters.Uri = reconnectUri;
} }
} }
@@ -242,6 +262,7 @@ namespace CryptoExchange.Net.Sockets
continue; continue;
} }
_lastReconnectTime = DateTime.UtcNow;
OnReconnected?.Invoke(); OnReconnected?.Invoke();
break; break;
} }
@@ -251,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual void Send(string data) public virtual void Send(int id, string data, int weight)
{ {
if (_ctsSource.IsCancellationRequested) if (_ctsSource.IsCancellationRequested)
return; return;
var bytes = Parameters.Encoding.GetBytes(data); var bytes = Parameters.Encoding.GetBytes(data);
_log.Write(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer"); _logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} bytes to send buffer");
_sendBuffer.Enqueue(bytes); _sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
_sendEvent.Set(); _sendEvent.Set();
} }
@@ -268,7 +289,7 @@ namespace CryptoExchange.Net.Sockets
if (_processState != ProcessState.Processing && IsOpen) if (_processState != ProcessState.Processing && IsOpen)
return; return;
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested"); _logger.Log(LogLevel.Debug, $"Socket {Id} reconnect requested");
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
await _closeTask.ConfigureAwait(false); await _closeTask.ConfigureAwait(false);
} }
@@ -277,24 +298,24 @@ namespace CryptoExchange.Net.Sockets
public virtual async Task CloseAsync() public virtual async Task CloseAsync()
{ {
await _closeSem.WaitAsync().ConfigureAwait(false); await _closeSem.WaitAsync().ConfigureAwait(false);
_stopRequested = true;
try 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, $"Socket {Id} CloseAsync() waiting for existing close task");
await _closeTask.ConfigureAwait(false); await _closeTask.ConfigureAwait(false);
return; return;
} }
_stopRequested = true;
if (!IsOpen) if (!IsOpen)
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open"); _logger.Log(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
return; return;
} }
_log.Write(LogLevel.Debug, $"Socket {Id} closing"); _logger.Log(LogLevel.Debug, $"Socket {Id} closing");
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
} }
finally finally
@@ -306,7 +327,7 @@ namespace CryptoExchange.Net.Sockets
if(_processTask != null) if(_processTask != null)
await _processTask.ConfigureAwait(false); await _processTask.ConfigureAwait(false);
OnClose?.Invoke(); OnClose?.Invoke();
_log.Write(LogLevel.Debug, $"Socket {Id} closed"); _logger.Log(LogLevel.Debug, $"Socket {Id} closed");
} }
/// <summary> /// <summary>
@@ -358,11 +379,11 @@ namespace CryptoExchange.Net.Sockets
if (_disposed) if (_disposed)
return; return;
_log.Write(LogLevel.Debug, $"Socket {Id} disposing"); _logger.Log(LogLevel.Debug, $"Socket {Id} disposing");
_disposed = true; _disposed = true;
_socket.Dispose(); _socket.Dispose();
_ctsSource.Dispose(); _ctsSource.Dispose();
_log.Write(LogLevel.Trace, $"Socket {Id} disposed"); _logger.Log(LogLevel.Trace, $"Socket {Id} disposed");
} }
/// <summary> /// <summary>
@@ -373,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
{ {
try try
{ {
var limitKey = Uri.ToString() + "/" + Id.ToString();
while (true) while (true)
{ {
if (_ctsSource.IsCancellationRequested) if (_ctsSource.IsCancellationRequested)
@@ -385,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
while (_sendBuffer.TryDequeue(out var data)) while (_sendBuffer.TryDequeue(out var data))
{ {
if (Parameters.RatelimitPerSecond != null) if (Parameters.RateLimiters != null)
{ {
// Wait for rate limit foreach(var ratelimiter in Parameters.RateLimiters)
DateTime? start = null;
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
{ {
start ??= DateTime.UtcNow; var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false); if (limitResult.Success)
{
if (limitResult.Data > 0)
_logger.Log(LogLevel.Debug, $"Socket {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 try
{ {
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false); await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
_outgoingMessages.Add(DateTime.UtcNow); OnRequestSent?.Invoke(data.Id);
_log.Write(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes"); _logger.Log(LogLevel.Trace, $"Socket {Id} - msg {data.Id} - sent {data.Bytes.Length} bytes");
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -414,6 +435,7 @@ namespace CryptoExchange.Net.Sockets
{ {
// Connection closed unexpectedly, .NET framework // Connection closed unexpectedly, .NET framework
OnError?.Invoke(ioe); OnError?.Invoke(ioe);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
break; break;
} }
@@ -425,12 +447,13 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed // 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. // 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 // Make sure we at least let the owner know there was an error
_logger.Log(LogLevel.Warning, $"Socket {Id} Send loop stopped with exception");
OnError?.Invoke(e); OnError?.Invoke(e);
throw; throw;
} }
finally finally
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} Send loop finished"); _logger.Log(LogLevel.Debug, $"Socket {Id} Send loop finished");
} }
} }
@@ -470,6 +493,7 @@ namespace CryptoExchange.Net.Sockets
{ {
// Connection closed unexpectedly // Connection closed unexpectedly
OnError?.Invoke(wse); OnError?.Invoke(wse);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
break; break;
} }
@@ -477,7 +501,8 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult.MessageType == WebSocketMessageType.Close) if (receiveResult.MessageType == WebSocketMessageType.Close)
{ {
// Connection closed unexpectedly // Connection closed unexpectedly
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message"); _logger.Log(LogLevel.Debug, $"Socket {Id} received `Close` message");
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
break; break;
} }
@@ -487,7 +512,7 @@ namespace CryptoExchange.Net.Sockets
// We received data, but it is not complete, write it to a memory stream for reassembling // We received data, but it is not complete, write it to a memory stream for reassembling
multiPartMessage = true; multiPartMessage = true;
memoryStream ??= new MemoryStream(); memoryStream ??= new MemoryStream();
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message"); _logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false); await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
} }
else else
@@ -495,13 +520,13 @@ namespace CryptoExchange.Net.Sockets
if (!multiPartMessage) if (!multiPartMessage)
{ {
// Received a complete message and it's not multi part // Received a complete message and it's not multi part
_log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message"); _logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType); HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
} }
else else
{ {
// Received the end of a multipart message, write to memory stream for reassembling // 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, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false); await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
} }
break; break;
@@ -529,12 +554,12 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult?.EndOfMessage == true) if (receiveResult?.EndOfMessage == true)
{ {
// Reassemble complete message from memory stream // Reassemble complete message from memory stream
_log.Write(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes"); _logger.Log(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType); HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
memoryStream.Dispose(); memoryStream.Dispose();
} }
else else
_log.Write(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes"); _logger.Log(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
} }
} }
} }
@@ -543,12 +568,13 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed // 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. // 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 // Make sure we at least let the owner know there was an error
_logger.Log(LogLevel.Warning, $"Socket {Id} Receive loop stopped with exception");
OnError?.Invoke(e); OnError?.Invoke(e);
throw; throw;
} }
finally finally
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} Receive loop finished"); _logger.Log(LogLevel.Debug, $"Socket {Id} Receive loop finished");
} }
} }
@@ -575,7 +601,7 @@ namespace CryptoExchange.Net.Sockets
} }
catch(Exception e) catch(Exception e)
{ {
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString()); _logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
return; return;
} }
} }
@@ -590,7 +616,7 @@ namespace CryptoExchange.Net.Sockets
} }
catch(Exception e) catch(Exception e)
{ {
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString()); _logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
return; return;
} }
} }
@@ -602,53 +628,17 @@ namespace CryptoExchange.Net.Sockets
} }
catch(Exception e) catch(Exception e)
{ {
_log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString()); _logger.Log(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)
{
LastActionTime = DateTime.UtcNow;
OnMessage?.Invoke(data);
}
/// <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> /// <summary>
/// Checks if there is no data received for a period longer than the specified timeout /// Checks if there is no data received for a period longer than the specified timeout
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected async Task CheckTimeoutAsync() protected async Task CheckTimeoutAsync()
{ {
_log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}"); _logger.Log(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
LastActionTime = DateTime.UtcNow; LastActionTime = DateTime.UtcNow;
try try
{ {
@@ -659,8 +649,8 @@ namespace CryptoExchange.Net.Sockets
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"); _logger.Log(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
_ = CloseAsync().ConfigureAwait(false); _ = ReconnectAsync().ConfigureAwait(false);
return; return;
} }
try try
@@ -690,20 +680,13 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
private static int NextStreamId() private static int NextStreamId()
{ {
lock (streamIdLock) lock (_streamIdLock)
{ {
lastStreamId++; _lastStreamId++;
return lastStreamId; return _lastStreamId;
} }
} }
private int MessagesSentLastSecond()
{
var testTime = DateTime.UtcNow;
_outgoingMessages.RemoveAll(r => testTime - r > TimeSpan.FromSeconds(1));
return _outgoingMessages.Count;
}
/// <summary> /// <summary>
/// Update the received messages list, removing messages received longer than 3s ago /// Update the received messages list, removing messages received longer than 3s ago
/// </summary> /// </summary>
@@ -713,8 +696,10 @@ namespace CryptoExchange.Net.Sockets
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1)) if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
{ {
foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list
{
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3)) if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
_receivedMessages.Remove(msg); _receivedMessages.Remove(msg);
}
_lastReceivedMessagesUpdate = checkTime; _lastReceivedMessagesUpdate = checkTime;
} }
@@ -723,25 +708,52 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Set proxy on socket /// Set proxy on socket
/// </summary> /// </summary>
/// <param name="socket"></param>
/// <param name="proxy"></param> /// <param name="proxy"></param>
/// <exception cref="ArgumentException"></exception> /// <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)) 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)); 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 = uri?.Scheme == null
? _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port) ? socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port)
: _socket.Options.Proxy = new WebProxy : socket.Options.Proxy = new WebProxy
{ {
Address = uri Address = uri
}; };
if (proxy.Login != null) 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> /// <summary>
/// Received message info /// Received message info
/// </summary> /// </summary>
+27 -17
View File
@@ -1,4 +1,5 @@
using System; using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -12,38 +13,34 @@ namespace CryptoExchange.Net.Sockets
/// The timestamp the data was received /// The timestamp the data was received
/// </summary> /// </summary>
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; set; }
/// <summary> /// <summary>
/// The topic of the update, what symbol/asset etc.. /// The topic of the update, what symbol/asset etc..
/// </summary> /// </summary>
public string? Topic { get; set; } public string? Topic { get; set; }
/// <summary> /// <summary>
/// The original data that was received, only available when OutputOriginalData is set to true in the client options /// The original data that was received, only available when OutputOriginalData is set to true in the client options
/// </summary> /// </summary>
public string? OriginalData { get; set; } public string? OriginalData { get; set; }
/// <summary>
/// Type of update
/// </summary>
public SocketUpdateType? UpdateType { get; set; }
/// <summary> /// <summary>
/// The received data deserialized into an object /// The received data deserialized into an object
/// </summary> /// </summary>
public T Data { get; set; } public T Data { get; set; }
internal DataEvent(T data, DateTime timestamp) internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
{
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)
{ {
Data = data; Data = data;
Topic = topic; Topic = topic;
OriginalData = originalData; OriginalData = originalData;
Timestamp = timestamp; Timestamp = timestamp;
UpdateType = updateType;
} }
/// <summary> /// <summary>
@@ -54,7 +51,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data) 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> /// <summary>
@@ -66,7 +63,20 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic) 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);
} }
} }
} }
@@ -12,14 +12,17 @@ namespace CryptoExchange.Net.Sockets
/// The connection the message was received on /// The connection the message was received on
/// </summary> /// </summary>
public SocketConnection Connection { get; set; } public SocketConnection Connection { get; set; }
/// <summary> /// <summary>
/// The json object of the data /// The json object of the data
/// </summary> /// </summary>
public JToken JsonData { get; set; } public JToken JsonData { get; set; }
/// <summary> /// <summary>
/// The originally received string data /// The originally received string data
/// </summary> /// </summary>
public string? OriginalData { get; set; } public string? OriginalData { get; set; }
/// <summary> /// <summary>
/// The timestamp of when the data was received /// The timestamp of when the data was received
/// </summary> /// </summary>
+18 -8
View File
@@ -7,27 +7,40 @@ namespace CryptoExchange.Net.Sockets
{ {
internal class PendingRequest internal class PendingRequest
{ {
public int Id { get; set; }
public Func<JToken, bool> Handler { get; } public Func<JToken, bool> Handler { get; }
public JToken? Result { get; private set; } public JToken? Result { get; private set; }
public bool Completed { get; private set; } public bool Completed { get; private set; }
public AsyncResetEvent Event { get; } public AsyncResetEvent Event { get; }
public DateTime RequestTimestamp { get; set; }
public TimeSpan Timeout { get; } public TimeSpan Timeout { get; }
public SocketSubscription? Subscription { get; }
private CancellationTokenSource cts; private CancellationTokenSource? _cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout) public PendingRequest(int id, Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
{ {
Id = id;
Handler = handler; Handler = handler;
Event = new AsyncResetEvent(false, false); Event = new AsyncResetEvent(false, false);
Timeout = timeout; Timeout = timeout;
RequestTimestamp = DateTime.UtcNow;
Subscription = subscription;
}
cts = new CancellationTokenSource(timeout); public void IsSend()
cts.Token.Register(Fail, false); {
// Start timeout countdown
_cts = new CancellationTokenSource(Timeout);
_cts.Token.Register(Fail, false);
} }
public bool CheckData(JToken data) public bool CheckData(JToken data)
{ {
if (Handler(data)) return Handler(data);
}
public bool Succeed(JToken data)
{ {
Result = data; Result = data;
Completed = true; Completed = true;
@@ -35,9 +48,6 @@ namespace CryptoExchange.Net.Sockets
return true; return true;
} }
return false;
}
public void Fail() public void Fail()
{ {
Completed = true; Completed = true;
+187 -103
View File
@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -53,8 +52,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public int SubscriptionCount public int SubscriptionCount
{ {
get { lock (subscriptionLock) get { lock (_subscriptionLock)
return subscriptions.Count(h => h.UserSubscription); } return _subscriptions.Count(h => h.UserSubscription); }
} }
/// <summary> /// <summary>
@@ -64,8 +63,8 @@ namespace CryptoExchange.Net.Sockets
{ {
get get
{ {
lock (subscriptionLock) lock (_subscriptionLock)
return subscriptions.Where(h => h.UserSubscription).ToArray(); return _subscriptions.Where(h => h.UserSubscription).ToArray();
} }
} }
@@ -109,19 +108,24 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public string Tag { get; set; } public string Tag { get; set; }
/// <summary>
/// Additional properties for this connection
/// </summary>
public Dictionary<string, object> Properties { get; set; }
/// <summary> /// <summary>
/// If activity is paused /// If activity is paused
/// </summary> /// </summary>
public bool PausedActivity public bool PausedActivity
{ {
get => pausedActivity; get => _pausedActivity;
set set
{ {
if (pausedActivity != value) if (_pausedActivity != value)
{ {
pausedActivity = value; _pausedActivity = value;
log.Write(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value); _logger.Log(LogLevel.Information, $"Socket {SocketId} Paused activity: " + value);
if(pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke()); if(_pausedActivity) _ = Task.Run(() => ActivityPaused?.Invoke());
else _ = Task.Run(() => ActivityUnpaused?.Invoke()); else _ = Task.Run(() => ActivityUnpaused?.Invoke());
} }
} }
@@ -140,18 +144,17 @@ namespace CryptoExchange.Net.Sockets
var oldStatus = _status; var oldStatus = _status;
_status = value; _status = value;
log.Write(LogLevel.Debug, $"Socket {SocketId} status changed from {oldStatus} to {_status}"); _logger.Log(LogLevel.Debug, $"Socket {SocketId} status changed from {oldStatus} to {_status}");
} }
} }
private bool pausedActivity; private bool _pausedActivity;
private readonly List<SocketSubscription> subscriptions; private readonly List<SocketSubscription> _subscriptions;
private readonly object subscriptionLock = new(); private readonly object _subscriptionLock = new();
private readonly Log log; private readonly ILogger _logger;
private readonly BaseSocketClient socketClient;
private readonly List<PendingRequest> pendingRequests; private readonly List<PendingRequest> _pendingRequests;
private SocketStatus _status; private SocketStatus _status;
@@ -163,22 +166,23 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// New socket connection /// New socket connection
/// </summary> /// </summary>
/// <param name="client">The socket client</param> /// <param name="logger">The logger</param>
/// <param name="apiClient">The api client</param> /// <param name="apiClient">The api client</param>
/// <param name="socket">The socket</param> /// <param name="socket">The socket</param>
/// <param name="tag"></param> /// <param name="tag"></param>
public SocketConnection(BaseSocketClient client, SocketApiClient apiClient, IWebsocket socket, string tag) public SocketConnection(ILogger logger, SocketApiClient apiClient, IWebsocket socket, string tag)
{ {
log = client.log; _logger = logger;
socketClient = client;
ApiClient = apiClient; ApiClient = apiClient;
Tag = tag; Tag = tag;
Properties = new Dictionary<string, object>();
pendingRequests = new List<PendingRequest>(); _pendingRequests = new List<PendingRequest>();
subscriptions = new List<SocketSubscription>(); _subscriptions = new List<SocketSubscription>();
_socket = socket; _socket = socket;
_socket.OnMessage += HandleMessage; _socket.OnMessage += HandleMessage;
_socket.OnRequestSent += HandleRequestSent;
_socket.OnOpen += HandleOpen; _socket.OnOpen += HandleOpen;
_socket.OnClose += HandleClose; _socket.OnClose += HandleClose;
_socket.OnReconnecting += HandleReconnecting; _socket.OnReconnecting += HandleReconnecting;
@@ -203,9 +207,9 @@ namespace CryptoExchange.Net.Sockets
{ {
Status = SocketStatus.Closed; Status = SocketStatus.Closed;
Authenticated = false; Authenticated = false;
lock(subscriptionLock) lock(_subscriptionLock)
{ {
foreach (var sub in subscriptions) foreach (var sub in _subscriptions)
sub.Confirmed = false; sub.Confirmed = false;
} }
Task.Run(() => ConnectionClosed?.Invoke()); Task.Run(() => ConnectionClosed?.Invoke());
@@ -219,9 +223,9 @@ namespace CryptoExchange.Net.Sockets
Status = SocketStatus.Reconnecting; Status = SocketStatus.Reconnecting;
DisconnectTime = DateTime.UtcNow; DisconnectTime = DateTime.UtcNow;
Authenticated = false; Authenticated = false;
lock (subscriptionLock) lock (_subscriptionLock)
{ {
foreach (var sub in subscriptions) foreach (var sub in _subscriptions)
sub.Confirmed = false; sub.Confirmed = false;
} }
@@ -234,7 +238,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
protected virtual async Task<Uri?> GetReconnectionUrlAsync() protected virtual async Task<Uri?> GetReconnectionUrlAsync()
{ {
return await socketClient.GetReconnectUriAsync(ApiClient, this).ConfigureAwait(false); return await ApiClient.GetReconnectUriAsync(this).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -243,18 +247,21 @@ namespace CryptoExchange.Net.Sockets
protected virtual async void HandleReconnected() protected virtual async void HandleReconnected()
{ {
Status = SocketStatus.Resubscribing; Status = SocketStatus.Resubscribing;
lock (pendingRequests) lock (_pendingRequests)
{ {
foreach (var pendingRequest in pendingRequests.ToList()) foreach (var pendingRequest in _pendingRequests.ToList())
{ {
pendingRequest.Fail(); pendingRequest.Fail();
pendingRequests.Remove(pendingRequest); _pendingRequests.Remove(pendingRequest);
} }
} }
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false); var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful) if (!reconnectSuccessful)
{
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
await _socket.ReconnectAsync().ConfigureAwait(false); await _socket.ReconnectAsync().ConfigureAwait(false);
}
else else
{ {
Status = SocketStatus.Connected; Status = SocketStatus.Connected;
@@ -273,9 +280,28 @@ namespace CryptoExchange.Net.Sockets
protected virtual void HandleError(Exception e) protected virtual void HandleError(Exception e)
{ {
if (e is WebSocketException wse) if (e is WebSocketException wse)
log.Write(LogLevel.Warning, $"Socket {SocketId} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString()); _logger.Log(LogLevel.Warning, $"Socket {SocketId} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
else else
log.Write(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString()); _logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
}
/// <summary>
/// Handler for whenever a request is sent over the websocket
/// </summary>
/// <param name="requestId">Id of the request sent</param>
protected virtual void HandleRequestSent(int requestId)
{
PendingRequest pendingRequest;
lock (_pendingRequests)
pendingRequest = _pendingRequests.SingleOrDefault(p => p.Id == requestId);
if (pendingRequest == null)
{
_logger.Log(LogLevel.Debug, $"Socket {SocketId} - msg {requestId} - message sent, but not pending");
return;
}
pendingRequest.IsSend();
} }
/// <summary> /// <summary>
@@ -285,14 +311,14 @@ namespace CryptoExchange.Net.Sockets
protected virtual void HandleMessage(string data) protected virtual void HandleMessage(string data)
{ {
var timestamp = DateTime.UtcNow; var timestamp = DateTime.UtcNow;
log.Write(LogLevel.Trace, $"Socket {SocketId} received data: " + data); _logger.Log(LogLevel.Trace, $"Socket {SocketId} received data: " + data);
if (string.IsNullOrEmpty(data)) return; if (string.IsNullOrEmpty(data)) return;
var tokenData = data.ToJToken(log); var tokenData = data.ToJToken(_logger);
if (tokenData == null) if (tokenData == null)
{ {
data = $"\"{data}\""; data = $"\"{data}\"";
tokenData = data.ToJToken(log); tokenData = data.ToJToken(_logger);
if (tokenData == null) if (tokenData == null)
return; return;
} }
@@ -301,10 +327,12 @@ namespace CryptoExchange.Net.Sockets
// Remove any timed out requests // Remove any timed out requests
PendingRequest[] requests; PendingRequest[] requests;
lock (pendingRequests) lock (_pendingRequests)
{ {
pendingRequests.RemoveAll(r => r.Completed); // Remove only timed out requests after 5 minutes have passed so we can still process any
requests = pendingRequests.ToArray(); // message coming in after the request timeout
_pendingRequests.RemoveAll(r => r.Completed && DateTime.UtcNow - r.RequestTimestamp > TimeSpan.FromMinutes(5));
requests = _pendingRequests.ToArray();
} }
// Check if this message is an answer on any pending requests // Check if this message is an answer on any pending requests
@@ -312,10 +340,25 @@ namespace CryptoExchange.Net.Sockets
{ {
if (pendingRequest.CheckData(tokenData)) if (pendingRequest.CheckData(tokenData))
{ {
lock (pendingRequests) lock (_pendingRequests)
pendingRequests.Remove(pendingRequest); _pendingRequests.Remove(pendingRequest);
if (!socketClient.ContinueOnQueryResponse) if (pendingRequest.Completed)
{
// Answer to a timed out request, unsub if it is a subscription request
if (pendingRequest.Subscription != null)
{
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Received subscription info after request timed out; unsubscribing. Consider increasing the RequestTimeout");
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
}
}
else
{
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {pendingRequest.Id} - received data matched to pending request");
pendingRequest.Succeed(tokenData);
}
if (!ApiClient.ContinueOnQueryResponse)
return; return;
handledResponse = true; handledResponse = true;
@@ -324,21 +367,23 @@ namespace CryptoExchange.Net.Sockets
} }
// Message was not a request response, check data handlers // Message was not a request response, check data handlers
var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data : null, timestamp); var messageEvent = new MessageEvent(this, tokenData, ApiClient.OutputOriginalData ? data : null, timestamp);
var (handled, userProcessTime, subscription) = HandleData(messageEvent); var (handled, userProcessTime, subscription) = HandleData(messageEvent);
if (!handled && !handledResponse) if (!handled && !handledResponse)
{ {
if (!socketClient.UnhandledMessageExpected) if (!ApiClient.UnhandledMessageExpected)
log.Write(LogLevel.Warning, $"Socket {SocketId} Message not handled: " + tokenData); _logger.Log(LogLevel.Warning, $"Socket {SocketId} Message not handled: " + tokenData);
UnhandledMessage?.Invoke(tokenData); UnhandledMessage?.Invoke(tokenData);
} }
var total = DateTime.UtcNow - timestamp; var total = DateTime.UtcNow - timestamp;
if (userProcessTime.TotalMilliseconds > 500) if (userProcessTime.TotalMilliseconds > 500)
log.Write(LogLevel.Debug, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processing slow ({(int)total.TotalMilliseconds}ms, {(int)userProcessTime.TotalMilliseconds}ms user code), consider offloading data handling to another thread. " + {
_logger.Log(LogLevel.Debug, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processing slow ({(int)total.TotalMilliseconds}ms, {(int)userProcessTime.TotalMilliseconds}ms user code), consider offloading data handling to another thread. " +
"Data from this socket may arrive late or not at all if message processing is continuously slow."); "Data from this socket may arrive late or not at all if message processing is continuously slow.");
}
log.Write(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms, ({(int)userProcessTime.TotalMilliseconds}ms user code)"); _logger.Log(LogLevel.Trace, $"Socket {SocketId}{(subscription == null ? "" : " subscription " + subscription!.Id)} message processed in {(int)total.TotalMilliseconds}ms ({(int)userProcessTime.TotalMilliseconds}ms user code)");
} }
/// <summary> /// <summary>
@@ -368,12 +413,12 @@ namespace CryptoExchange.Net.Sockets
if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed) if (Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
return; return;
if (socketClient.socketConnections.ContainsKey(SocketId)) if (ApiClient.socketConnections.ContainsKey(SocketId))
socketClient.socketConnections.TryRemove(SocketId, out _); ApiClient.socketConnections.TryRemove(SocketId, out _);
lock (subscriptionLock) lock (_subscriptionLock)
{ {
foreach (var subscription in subscriptions) foreach (var subscription in _subscriptions)
{ {
if (subscription.CancellationTokenRegistration.HasValue) if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose(); subscription.CancellationTokenRegistration.Value.Dispose();
@@ -391,9 +436,9 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public async Task CloseAsync(SocketSubscription subscription) public async Task CloseAsync(SocketSubscription subscription)
{ {
lock (subscriptionLock) lock (_subscriptionLock)
{ {
if (!subscriptions.Contains(subscription)) if (!_subscriptions.Contains(subscription))
return; return;
subscription.Closed = true; subscription.Closed = true;
@@ -402,35 +447,35 @@ namespace CryptoExchange.Net.Sockets
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
return; return;
log.Write(LogLevel.Debug, $"Socket {SocketId} closing subscription {subscription.Id}"); _logger.Log(LogLevel.Debug, $"Socket {SocketId} closing subscription {subscription.Id}");
if (subscription.CancellationTokenRegistration.HasValue) if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose(); subscription.CancellationTokenRegistration.Value.Dispose();
if (subscription.Confirmed && _socket.IsOpen) if (subscription.Confirmed && _socket.IsOpen)
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false); await ApiClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
bool shouldCloseConnection; bool shouldCloseConnection;
lock (subscriptionLock) lock (_subscriptionLock)
{ {
if (Status == SocketStatus.Closing) if (Status == SocketStatus.Closing)
{ {
log.Write(LogLevel.Debug, $"Socket {SocketId} already closing"); _logger.Log(LogLevel.Debug, $"Socket {SocketId} already closing");
return; return;
} }
shouldCloseConnection = subscriptions.All(r => !r.UserSubscription || r.Closed); shouldCloseConnection = _subscriptions.All(r => !r.UserSubscription || r.Closed);
if (shouldCloseConnection) if (shouldCloseConnection)
Status = SocketStatus.Closing; Status = SocketStatus.Closing;
} }
if (shouldCloseConnection) if (shouldCloseConnection)
{ {
log.Write(LogLevel.Debug, $"Socket {SocketId} closing as there are no more subscriptions"); _logger.Log(LogLevel.Debug, $"Socket {SocketId} closing as there are no more subscriptions");
await CloseAsync().ConfigureAwait(false); await CloseAsync().ConfigureAwait(false);
} }
lock (subscriptionLock) lock (_subscriptionLock)
subscriptions.Remove(subscription); _subscriptions.Remove(subscription);
} }
/// <summary> /// <summary>
@@ -448,14 +493,14 @@ namespace CryptoExchange.Net.Sockets
/// <param name="subscription"></param> /// <param name="subscription"></param>
public bool AddSubscription(SocketSubscription subscription) public bool AddSubscription(SocketSubscription subscription)
{ {
lock (subscriptionLock) lock (_subscriptionLock)
{ {
if (Status != SocketStatus.None && Status != SocketStatus.Connected) if (Status != SocketStatus.None && Status != SocketStatus.Connected)
return false; return false;
subscriptions.Add(subscription); _subscriptions.Add(subscription);
if(subscription.UserSubscription) if(subscription.UserSubscription)
log.Write(LogLevel.Debug, $"Socket {SocketId} adding new subscription with id {subscription.Id}, total subscriptions on connection: {subscriptions.Count(s => s.UserSubscription)}"); _logger.Log(LogLevel.Debug, $"Socket {SocketId} adding new subscription with id {subscription.Id}, total subscriptions on connection: {_subscriptions.Count(s => s.UserSubscription)}");
return true; return true;
} }
} }
@@ -466,8 +511,8 @@ namespace CryptoExchange.Net.Sockets
/// <param name="id"></param> /// <param name="id"></param>
public SocketSubscription? GetSubscription(int id) public SocketSubscription? GetSubscription(int id)
{ {
lock (subscriptionLock) lock (_subscriptionLock)
return subscriptions.SingleOrDefault(s => s.Id == id); return _subscriptions.SingleOrDefault(s => s.Id == id);
} }
/// <summary> /// <summary>
@@ -477,8 +522,8 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public SocketSubscription? GetSubscriptionByRequest(Func<object?, bool> predicate) public SocketSubscription? GetSubscriptionByRequest(Func<object?, bool> predicate)
{ {
lock(subscriptionLock) lock(_subscriptionLock)
return subscriptions.SingleOrDefault(s => predicate(s.Request)); return _subscriptions.SingleOrDefault(s => predicate(s.Request));
} }
/// <summary> /// <summary>
@@ -496,15 +541,15 @@ namespace CryptoExchange.Net.Sockets
// Loop the subscriptions to check if any of them signal us that the message is for them // Loop the subscriptions to check if any of them signal us that the message is for them
List<SocketSubscription> subscriptionsCopy; List<SocketSubscription> subscriptionsCopy;
lock (subscriptionLock) lock (_subscriptionLock)
subscriptionsCopy = subscriptions.ToList(); subscriptionsCopy = _subscriptions.ToList();
foreach (var subscription in subscriptionsCopy) foreach (var subscription in subscriptionsCopy)
{ {
currentSubscription = subscription; currentSubscription = subscription;
if (subscription.Request == null) if (subscription.Request == null)
{ {
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!)) if (ApiClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!))
{ {
handled = true; handled = true;
var userSw = Stopwatch.StartNew(); var userSw = Stopwatch.StartNew();
@@ -515,10 +560,10 @@ namespace CryptoExchange.Net.Sockets
} }
else else
{ {
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Request)) if (ApiClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Request))
{ {
handled = true; handled = true;
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData); messageEvent.JsonData = ApiClient.ProcessTokenData(messageEvent.JsonData);
var userSw = Stopwatch.StartNew(); var userSw = Stopwatch.StartNew();
subscription.MessageHandler(messageEvent); subscription.MessageHandler(messageEvent);
userSw.Stop(); userSw.Stop();
@@ -531,7 +576,7 @@ namespace CryptoExchange.Net.Sockets
} }
catch (Exception ex) catch (Exception ex)
{ {
log.Write(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}"); _logger.Log(LogLevel.Error, $"Socket {SocketId} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}");
currentSubscription?.InvokeExceptionHandler(ex); currentSubscription?.InvokeExceptionHandler(ex);
return (false, TimeSpan.Zero, null); return (false, TimeSpan.Zero, null);
} }
@@ -543,46 +588,71 @@ namespace CryptoExchange.Net.Sockets
/// <typeparam name="T">The data type expected in response</typeparam> /// <typeparam name="T">The data type expected in response</typeparam>
/// <param name="obj">The object to send</param> /// <param name="obj">The object to send</param>
/// <param name="timeout">The timeout for response</param> /// <param name="timeout">The timeout for response</param>
/// <param name="subscription">Subscription if this is a subscribe request</param>
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param> /// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
/// <param name="weight">The weight of the message</param>
/// <returns></returns> /// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler) public virtual async Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, int weight, Func<JToken, bool> handler)
{ {
var pending = new PendingRequest(handler, timeout); var pending = new PendingRequest(ExchangeHelpers.NextId(), handler, timeout, subscription);
lock (pendingRequests) lock (_pendingRequests)
{ {
pendingRequests.Add(pending); _pendingRequests.Add(pending);
} }
var sendOk = Send(obj);
if(!sendOk)
pending.Fail();
return pending.Event.WaitAsync(timeout); var sendOk = Send(pending.Id, obj, weight);
if (!sendOk)
{
pending.Fail();
return;
}
while (true)
{
if(!_socket.IsOpen)
{
pending.Fail();
return;
}
if (pending.Completed)
return;
await pending.Event.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
if (pending.Completed)
return;
}
} }
/// <summary> /// <summary>
/// Send data over the websocket connection /// Send data over the websocket connection
/// </summary> /// </summary>
/// <typeparam name="T">The type of the object to send</typeparam> /// <typeparam name="T">The type of the object to send</typeparam>
/// <param name="requestId">The request id</param>
/// <param name="obj">The object to send</param> /// <param name="obj">The object to send</param>
/// <param name="nullValueHandling">How null values should be serialized</param> /// <param name="nullValueHandling">How null values should be serialized</param>
public virtual bool Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore) /// <param name="weight">The weight of the message</param>
public virtual bool Send<T>(int requestId, T obj, int weight, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
{ {
if(obj is string str) if(obj is string str)
return Send(str); return Send(requestId, str, weight);
else else
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling })); return Send(requestId, JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }), weight);
} }
/// <summary> /// <summary>
/// Send string data over the websocket connection /// Send string data over the websocket connection
/// </summary> /// </summary>
/// <param name="data">The data to send</param> /// <param name="data">The data to send</param>
public virtual bool Send(string data) /// <param name="weight">The weight of the message</param>
/// <param name="requestId">The id of the request</param>
public virtual bool Send(int requestId, string data, int weight)
{ {
log.Write(LogLevel.Trace, $"Socket {SocketId} sending data: {data}"); _logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {requestId} - sending messsage: {data}");
try try
{ {
_socket.Send(data); _socket.Send(requestId, data, weight);
return true; return true;
} }
catch(Exception) catch(Exception)
@@ -597,36 +667,40 @@ namespace CryptoExchange.Net.Sockets
return new CallResult<bool>(new WebError("Socket not connected")); return new CallResult<bool>(new WebError("Socket not connected"));
bool anySubscriptions = false; bool anySubscriptions = false;
lock (subscriptionLock) lock (_subscriptionLock)
anySubscriptions = subscriptions.Any(s => s.UserSubscription); anySubscriptions = _subscriptions.Any(s => s.UserSubscription);
if (!anySubscriptions) if (!anySubscriptions)
{ {
// No need to resubscribe anything // No need to resubscribe anything
log.Write(LogLevel.Debug, $"Socket {SocketId} Nothing to resubscribe, closing connection"); _logger.Log(LogLevel.Debug, $"Socket {SocketId} Nothing to resubscribe, closing connection");
_ = _socket.CloseAsync(); _ = _socket.CloseAsync();
return new CallResult<bool>(true); return new CallResult<bool>(true);
} }
if (subscriptions.Any(s => s.Authenticated)) bool anyAuthenticated = false;
lock (_subscriptionLock)
anyAuthenticated = _subscriptions.Any(s => s.Authenticated);
if (anyAuthenticated)
{ {
// If we reconnected a authenticated connection we need to re-authenticate // If we reconnected a authenticated connection we need to re-authenticate
var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false); var authResult = await ApiClient.AuthenticateSocketAsync(this).ConfigureAwait(false);
if (!authResult) if (!authResult)
{ {
log.Write(LogLevel.Warning, $"Socket {SocketId} authentication failed on reconnected socket. Disconnecting and reconnecting."); _logger.Log(LogLevel.Warning, $"Socket {SocketId} authentication failed on reconnected socket. Disconnecting and reconnecting.");
return authResult; return authResult;
} }
Authenticated = true; Authenticated = true;
log.Write(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket."); _logger.Log(LogLevel.Debug, $"Socket {SocketId} authentication succeeded on reconnected socket.");
} }
// Get a list of all subscriptions on the socket // Get a list of all subscriptions on the socket
List<SocketSubscription> subscriptionList = new List<SocketSubscription>(); List<SocketSubscription> subscriptionList = new List<SocketSubscription>();
lock (subscriptionLock) lock (_subscriptionLock)
{ {
foreach (var subscription in subscriptions) foreach (var subscription in _subscriptions)
{ {
if (subscription.Request != null) if (subscription.Request != null)
subscriptionList.Add(subscription); subscriptionList.Add(subscription);
@@ -635,15 +709,25 @@ namespace CryptoExchange.Net.Sockets
} }
} }
foreach(var subscription in subscriptionList.Where(s => s.Request != null))
{
var result = await ApiClient.RevitalizeRequestAsync(subscription.Request!).ConfigureAwait(false);
if (!result)
{
_logger.Log(LogLevel.Warning, $"Socket {SocketId} Failed request revitalization: " + result.Error);
return result.As<bool>(false);
}
}
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe // Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
for (var i = 0; i < subscriptionList.Count; i += socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket) for (var i = 0; i < subscriptionList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
{ {
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected")); return new CallResult<bool>(new WebError("Socket not connected"));
var taskList = new List<Task<CallResult<bool>>>(); var taskList = new List<Task<CallResult<bool>>>();
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)) foreach (var subscription in subscriptionList.Skip(i).Take(ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
taskList.Add(socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription)); taskList.Add(ApiClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription));
await Task.WhenAll(taskList).ConfigureAwait(false); await Task.WhenAll(taskList).ConfigureAwait(false);
if (taskList.Any(t => !t.Result.Success)) if (taskList.Any(t => !t.Result.Success))
@@ -656,13 +740,13 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult<bool>(new WebError("Socket not connected")); return new CallResult<bool>(new WebError("Socket not connected"));
log.Write(LogLevel.Debug, $"Socket {SocketId} all subscription successfully resubscribed on reconnected socket."); _logger.Log(LogLevel.Debug, $"Socket {SocketId} all subscription successfully resubscribed on reconnected socket.");
return new CallResult<bool>(true); return new CallResult<bool>(true);
} }
internal async Task UnsubscribeAsync(SocketSubscription socketSubscription) internal async Task UnsubscribeAsync(SocketSubscription socketSubscription)
{ {
await socketClient.UnsubscribeAsync(this, socketSubscription).ConfigureAwait(false); await ApiClient.UnsubscribeAsync(this, socketSubscription).ConfigureAwait(false);
} }
internal async Task<CallResult<bool>> ResubscribeAsync(SocketSubscription socketSubscription) internal async Task<CallResult<bool>> ResubscribeAsync(SocketSubscription socketSubscription)
@@ -670,7 +754,7 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult<bool>(new UnknownError("Socket is not connected")); return new CallResult<bool>(new UnknownError("Socket is not connected"));
return await socketClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false); return await ApiClient.SubscribeAndWaitAsync(this, socketSubscription.Request!, socketSubscription).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -9,16 +9,16 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public class UpdateSubscription public class UpdateSubscription
{ {
private readonly SocketConnection connection; private readonly SocketConnection _connection;
private readonly SocketSubscription subscription; private readonly SocketSubscription _subscription;
/// <summary> /// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible. /// Event when the connection is lost. The socket will automatically reconnect when possible.
/// </summary> /// </summary>
public event Action ConnectionLost public event Action ConnectionLost
{ {
add => connection.ConnectionLost += value; add => _connection.ConnectionLost += value;
remove => connection.ConnectionLost -= value; remove => _connection.ConnectionLost -= value;
} }
/// <summary> /// <summary>
@@ -26,8 +26,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public event Action ConnectionClosed public event Action ConnectionClosed
{ {
add => connection.ConnectionClosed += value; add => _connection.ConnectionClosed += value;
remove => connection.ConnectionClosed -= value; remove => _connection.ConnectionClosed -= value;
} }
/// <summary> /// <summary>
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public event Action<TimeSpan> ConnectionRestored public event Action<TimeSpan> ConnectionRestored
{ {
add => connection.ConnectionRestored += value; add => _connection.ConnectionRestored += value;
remove => connection.ConnectionRestored -= value; remove => _connection.ConnectionRestored -= value;
} }
/// <summary> /// <summary>
@@ -46,8 +46,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public event Action ActivityPaused public event Action ActivityPaused
{ {
add => connection.ActivityPaused += value; add => _connection.ActivityPaused += value;
remove => connection.ActivityPaused -= value; remove => _connection.ActivityPaused -= value;
} }
/// <summary> /// <summary>
@@ -55,8 +55,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public event Action ActivityUnpaused public event Action ActivityUnpaused
{ {
add => connection.ActivityUnpaused += value; add => _connection.ActivityUnpaused += value;
remove => connection.ActivityUnpaused -= value; remove => _connection.ActivityUnpaused -= value;
} }
/// <summary> /// <summary>
@@ -64,19 +64,19 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public event Action<Exception> Exception public event Action<Exception> Exception
{ {
add => subscription.Exception += value; add => _subscription.Exception += value;
remove => subscription.Exception -= value; remove => _subscription.Exception -= value;
} }
/// <summary> /// <summary>
/// The id of the socket /// The id of the socket
/// </summary> /// </summary>
public int SocketId => connection.SocketId; public int SocketId => _connection.SocketId;
/// <summary> /// <summary>
/// The id of the subscription /// The id of the subscription
/// </summary> /// </summary>
public int Id => subscription.Id; public int Id => _subscription.Id;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -85,8 +85,8 @@ namespace CryptoExchange.Net.Sockets
/// <param name="subscription">The subscription</param> /// <param name="subscription">The subscription</param>
public UpdateSubscription(SocketConnection connection, SocketSubscription subscription) public UpdateSubscription(SocketConnection connection, SocketSubscription subscription)
{ {
this.connection = connection; this._connection = connection;
this.subscription = subscription; this._subscription = subscription;
} }
/// <summary> /// <summary>
@@ -95,7 +95,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public Task CloseAsync() public Task CloseAsync()
{ {
return connection.CloseAsync(subscription); return _connection.CloseAsync(_subscription);
} }
/// <summary> /// <summary>
@@ -104,7 +104,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public Task ReconnectAsync() public Task ReconnectAsync()
{ {
return connection.TriggerReconnectAsync(); return _connection.TriggerReconnectAsync();
} }
/// <summary> /// <summary>
@@ -113,7 +113,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
internal async Task UnsubscribeAsync() internal async Task UnsubscribeAsync()
{ {
await connection.UnsubscribeAsync(subscription).ConfigureAwait(false); await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -122,7 +122,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
internal async Task<CallResult<bool>> ResubscribeAsync() internal async Task<CallResult<bool>> ResubscribeAsync()
{ {
return await connection.ResubscribeAsync(subscription).ConfigureAwait(false); return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
} }
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@@ -15,42 +16,52 @@ namespace CryptoExchange.Net.Sockets
/// The uri to connect to /// The uri to connect to
/// </summary> /// </summary>
public Uri Uri { get; set; } public Uri Uri { get; set; }
/// <summary> /// <summary>
/// Headers to send in the connection handshake /// Headers to send in the connection handshake
/// </summary> /// </summary>
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
/// <summary> /// <summary>
/// Cookies to send in the connection handshake /// Cookies to send in the connection handshake
/// </summary> /// </summary>
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
/// <summary> /// <summary>
/// The time to wait between reconnect attempts /// The time to wait between reconnect attempts
/// </summary> /// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary> /// <summary>
/// Proxy for the connection /// Proxy for the connection
/// </summary> /// </summary>
public ApiProxy? Proxy { get; set; } public ApiProxy? Proxy { get; set; }
/// <summary> /// <summary>
/// Whether the socket should automatically reconnect when connection is lost /// Whether the socket should automatically reconnect when connection is lost
/// </summary> /// </summary>
public bool AutoReconnect { get; set; } public bool AutoReconnect { get; set; }
/// <summary> /// <summary>
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket /// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
/// </summary> /// </summary>
public TimeSpan? Timeout { get; set; } public TimeSpan? Timeout { get; set; }
/// <summary> /// <summary>
/// Interval at which to send ping frames /// Interval at which to send ping frames
/// </summary> /// </summary>
public TimeSpan? KeepAliveInterval { get; set; } public TimeSpan? KeepAliveInterval { get; set; }
/// <summary> /// <summary>
/// The max amount of messages to send per second /// The rate limiters for the socket connection
/// </summary> /// </summary>
public int? RatelimitPerSecond { get; set; } public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary> /// <summary>
/// Origin header value to send in the connection handshake /// Origin header value to send in the connection handshake
/// </summary> /// </summary>
public string? Origin { get; set; } public string? Origin { get; set; }
/// <summary> /// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers /// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary> /// </summary>
@@ -1,5 +1,5 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging; using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -9,9 +9,9 @@ namespace CryptoExchange.Net.Sockets
public class WebsocketFactory : IWebsocketFactory public class WebsocketFactory : IWebsocketFactory
{ {
/// <inheritdoc /> /// <inheritdoc />
public IWebsocket CreateWebsocket(Log log, WebSocketParameters parameters) public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters)
{ {
return new CryptoExchangeWebSocketClient(log, parameters); return new CryptoExchangeWebSocketClient(logger, parameters);
} }
} }
} }
+11 -14
View File
@@ -5,20 +5,17 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="8.0.6" /> <PackageReference Include="Binance.Net" Version="9.1.5" />
<PackageReference Include="Bitfinex.Net" Version="5.0.3" /> <PackageReference Include="Bitfinex.Net" Version="7.0.4" />
<PackageReference Include="Bittrex.Net" Version="7.0.4" /> <PackageReference Include="Bittrex.Net" Version="8.0.3" />
<PackageReference Include="Bybit.Net" Version="0.0.4" /> <PackageReference Include="Bybit.Net" Version="3.2.1" />
<PackageReference Include="CoinEx.Net" Version="5.0.3" /> <PackageReference Include="CoinEx.Net" Version="6.0.3" />
<PackageReference Include="FTX.Net" Version="1.0.4" /> <PackageReference Include="Huobi.Net" Version="5.0.3" />
<PackageReference Include="Huobi.Net" Version="4.0.4" /> <PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" /> <PackageReference Include="JK.OKX.Net" Version="1.4.2" />
<PackageReference Include="Kucoin.Net" Version="4.0.3" /> <PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00250" /> <PackageReference Include="Kucoin.Net" Version="5.0.5" />
</ItemGroup> <PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+21 -16
View File
@@ -1,13 +1,14 @@
@page "/" @page "/"
@inject IBinanceClient binanceClient @inject IBinanceRestClient binanceClient
@inject IBitfinexClient bitfinexClient @inject IBitfinexRestClient bitfinexClient
@inject IBittrexClient bittrexClient @inject IBitgetRestClient bitgetClient
@inject IBybitClient bybitClient @inject IBittrexRestClient bittrexClient
@inject ICoinExClient coinexClient @inject IBybitRestClient bybitClient
@inject IFTXClient ftxClient @inject ICoinExRestClient coinexClient
@inject IHuobiClient huobiClient @inject IHuobiRestClient huobiClient
@inject IKrakenClient krakenClient @inject IKrakenRestClient krakenClient
@inject IKucoinClient kucoinClient @inject IKucoinRestClient kucoinClient
@inject IOKXRestClient okxClient
<h3>BTC-USD prices:</h3> <h3>BTC-USD prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key)) @foreach(var price in _prices.OrderBy(p => p.Key))
@@ -22,15 +23,16 @@
{ {
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD"); var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var bybitTask = bybitClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var ftxTask = ftxClient.TradeApi.ExchangeData.GetSymbolAsync("BTC/USD");
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt"); var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD"); var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, ftxTask, huobiTask, krakenTask, kucoinTask); await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
if (binanceTask.Result.Success) if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice); _prices.Add("Binance", binanceTask.Result.Data.LastPrice);
@@ -38,18 +40,18 @@
if (bitfinexTask.Result.Success) if (bitfinexTask.Result.Success)
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice); _prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
if (bitgetTask.Result.Success)
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
if (bittrexTask.Result.Success) if (bittrexTask.Result.Success)
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice); _prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
if (bybitTask.Result.Success) if (bybitTask.Result.Success)
_prices.Add("Bybit", bybitTask.Result.Data.LastPrice); _prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
if (coinexTask.Result.Success) if (coinexTask.Result.Success)
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice); _prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
if (ftxTask.Result.Success)
_prices.Add("FTX", ftxTask.Result.Data.LastPrice ?? 0);
if (huobiTask.Result.Success) if (huobiTask.Result.Success)
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0); _prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
@@ -58,6 +60,9 @@
if (kucoinTask.Result.Success) if (kucoinTask.Result.Success)
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0); _prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
if (okxTask.Result.Success)
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
} }
} }
+13 -19
View File
@@ -1,24 +1,17 @@
@page "/LiveData" @page "/LiveData"
@inject IBinanceSocketClient binanceSocketClient @inject IBinanceSocketClient binanceSocketClient
@inject IBitfinexSocketClient bitfinexSocketClient @inject IBitfinexSocketClient bitfinexSocketClient
@inject IBitgetSocketClient bitgetSocketClient
@inject IBittrexSocketClient bittrexSocketClient @inject IBittrexSocketClient bittrexSocketClient
@inject IBybitSocketClient bybitSocketClient @inject IBybitSocketClient bybitSocketClient
@inject ICoinExSocketClient coinExSocketClient @inject ICoinExSocketClient coinExSocketClient
@inject IFTXSocketClient ftxSocketClient
@inject IHuobiSocketClient huobiSocketClient @inject IHuobiSocketClient huobiSocketClient
@inject IKrakenSocketClient krakenSocketClient @inject IKrakenSocketClient krakenSocketClient
@inject IKucoinSocketClient kucoinSocketClient @inject IKucoinSocketClient kucoinSocketClient
@using Binance.Net.Clients.SpotApi @inject IOKXSocketClient okxSocketClient
@using Bitfinex.Net.Clients.SpotApi @using System.Collections.Concurrent
@using Bittrex.Net.Clients.SpotApi
@using Bybit.Net.Clients.SpotApi
@using CoinEx.Net.Clients.SpotApi
@using CryptoExchange.Net.Objects @using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Sockets @using CryptoExchange.Net.Sockets
@using Huobi.Net.Clients.SpotApi
@using Kraken.Net.Clients.SpotApi
@using Kucoin.Net.Clients.SpotApi
@using System.Collections.Concurrent
@implements IDisposable @implements IDisposable
<h3>ETH-BTC prices, live updates:</h3> <h3>ETH-BTC prices, live updates:</h3>
@@ -35,15 +28,16 @@
{ {
var tasks = new Task<CallResult<UpdateSubscription>>[] var tasks = new Task<CallResult<UpdateSubscription>>[]
{ {
binanceSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)), binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
bitfinexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)), bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bittrexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)), bitgetSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
bybitSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)), bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
coinExSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)), bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
ftxSocketClient.Streams.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("FTX", data.Data.LastPrice ?? 0)), coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
huobiSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)), huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
krakenSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)), krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)), kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.LastPrice ?? 0)),
}; };
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
+35 -25
View File
@@ -1,19 +1,28 @@
@page "/OrderBooks" @page "/OrderBooks"
@using Binance.Net.SymbolOrderBooks
@using Bitfinex.Net.SymbolOrderBooks
@using Bittrex.Net.SymbolOrderBooks
@using Bybit.Net.SymbolOrderBooks
@using CryptoExchange.Net.Interfaces
@using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Sockets
@using CoinEx.Net.SymbolOrderBooks
@using FTX.Net.SymbolOrderBooks
@using Huobi.Net.SymbolOrderBooks
@using Kraken.Net.SymbolOrderBooks
@using Kucoin.Net.Clients
@using Kucoin.Net.SymbolOrderBooks
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using System.Timers @using System.Timers
@using Binance.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using Bittrex.Net.Interfaces
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CryptoExchange.Net.Interfaces
@using Huobi.Net.Interfaces
@using Kraken.Net.Interfaces
@using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces
@using OKX.Net.Interfaces;
@inject IBinanceOrderBookFactory binanceFactory
@inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBitgetOrderBookFactory bitgetFactory
@inject IBittrexOrderBookFactory bittrexFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject IHuobiOrderBookFactory huobiFactory
@inject IKrakenOrderBookFactory krakenFactory
@inject IKucoinOrderBookFactory kucoinFactory
@inject IOKXOrderBookFactory okxFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC books, live updates:</h3> <h3>ETH-BTC books, live updates:</h3>
@@ -40,22 +49,23 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand // Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
KucoinClient.SetDefaultOptions(new Kucoin.Net.Objects.KucoinClientOptions KucoinRestClient.SetDefaultOptions(options =>
{ {
ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE") options.ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE");
}); });
_books = new Dictionary<string, ISymbolOrderBook> _books = new Dictionary<string, ISymbolOrderBook>
{ {
{ "Binance", new BinanceSpotSymbolOrderBook("ETHBTC") }, { "Binance", binanceFactory.CreateSpot("ETHBTC") },
{ "Bitfinex", new BitfinexSymbolOrderBook("tETHBTC") }, { "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bittrex", new BittrexSymbolOrderBook("ETH-BTC") }, { "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "Bybit", new BybitSpotSymbolOrderBook("ETHBTC") }, { "Bittrex", bittrexFactory.Create("ETH-BTC") },
{ "CoinEx", new CoinExSpotSymbolOrderBook("ETHBTC") }, { "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "FTX", new FTXSymbolOrderBook("ETH/BTC") }, { "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "Huobi", new HuobiSpotSymbolOrderBook("ethbtc") }, { "Huobi", huobiFactory.CreateSpot("ethbtc") },
{ "Kraken", new KrakenSpotSymbolOrderBook("ETH/XBT") }, { "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
{ "Kucoin", new KucoinSpotSymbolOrderBook("ETH-BTC") }, { "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "OKX", okxFactory.Create("ETH-BTC") },
}; };
await Task.WhenAll(_books.Select(b => b.Value.StartAsync())); await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
@@ -70,7 +80,7 @@
{ {
_timer.Stop(); _timer.Stop();
_timer.Dispose(); _timer.Dispose();
foreach (var book in _books) foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
// It's not necessary to wait for this // It's not necessary to wait for this
_ = book.Value.StopAsync(); _ = book.Value.StopAsync();
} }
+16 -13
View File
@@ -1,24 +1,26 @@
@page "/SpotClient" @page "/SpotClient"
@inject IBinanceClient binanceClient @inject IBinanceRestClient binanceClient
@inject IBitfinexClient bitfinexClient @inject IBitfinexRestClient bitfinexClient
@inject IBittrexClient bittrexClient @inject IBitgetRestClient bitgetClient
@inject IBybitClient bybitClient @inject IBittrexRestClient bittrexClient
@inject ICoinExClient coinexClient @inject IBybitRestClient bybitClient
@inject IFTXClient ftxClient @inject ICoinExRestClient coinexClient
@inject IHuobiClient huobiClient @inject IHuobiRestClient huobiClient
@inject IKrakenClient krakenClient @inject IKrakenRestClient krakenClient
@inject IKucoinClient kucoinClient @inject IKucoinRestClient kucoinClient
@inject IOKXRestClient okxClient
@using Binance.Net.Clients.SpotApi @using Binance.Net.Clients.SpotApi
@using Bitfinex.Net.Clients.SpotApi @using Bitfinex.Net.Clients.SpotApi
@using Bittrex.Net.Clients.SpotApi @using Bittrex.Net.Clients.SpotApi
@using Bitget.Net.Clients.SpotApi
@using Bybit.Net.Clients.SpotApi @using Bybit.Net.Clients.SpotApi
@using CoinEx.Net.Clients.SpotApi @using CoinEx.Net.Clients.SpotApi
@using CryptoExchange.Net.Interfaces @using CryptoExchange.Net.Interfaces
@using CryptoExchange.Net.Interfaces.CommonClients @using CryptoExchange.Net.Interfaces.CommonClients
@using FTX.Net.Clients.TradeApi
@using Huobi.Net.Clients.SpotApi @using Huobi.Net.Clients.SpotApi
@using Kraken.Net.Clients.SpotApi @using Kraken.Net.Clients.SpotApi
@using Kucoin.Net.Clients.SpotApi @using Kucoin.Net.Clients.SpotApi
@using OKX.Net.Clients.UnifiedApi
<h3>ETH-BTC prices:</h3> <h3>ETH-BTC prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key)) @foreach(var price in _prices.OrderBy(p => p.Key))
@@ -36,13 +38,14 @@
binanceClient.SpotApi.CommonSpotClient, binanceClient.SpotApi.CommonSpotClient,
bitfinexClient.SpotApi.CommonSpotClient, bitfinexClient.SpotApi.CommonSpotClient,
bitgetClient.SpotApi.CommonSpotClient,
bittrexClient.SpotApi.CommonSpotClient, bittrexClient.SpotApi.CommonSpotClient,
bybitClient.SpotApi.CommonSpotClient, bybitClient.SpotApiV1.CommonSpotClient,
coinexClient.SpotApi.CommonSpotClient, coinexClient.SpotApi.CommonSpotClient,
ftxClient.TradeApi.CommonSpotClient,
huobiClient.SpotApi.CommonSpotClient, huobiClient.SpotApi.CommonSpotClient,
krakenClient.SpotApi.CommonSpotClient, krakenClient.SpotApi.CommonSpotClient,
kucoinClient.SpotApi.CommonSpotClient kucoinClient.SpotApi.CommonSpotClient,
okxClient.UnifiedApi.CommonSpotClient
}; };
var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC")))); var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC"))));
+8 -25
View File
@@ -2,15 +2,12 @@ using System.Collections.Generic;
using Binance.Net; using Binance.Net;
using Binance.Net.Clients; using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients; using Binance.Net.Interfaces.Clients;
using Binance.Net.Objects;
using Bitfinex.Net; using Bitfinex.Net;
using Bitget.Net;
using Bittrex.Net; using Bittrex.Net;
using Bybit.Net; using Bybit.Net;
using CoinEx.Net; using CoinEx.Net;
using CoinEx.Net.Clients;
using CoinEx.Net.Interfaces.Clients;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using FTX.Net;
using Huobi.Net; using Huobi.Net;
using Kraken.Net; using Kraken.Net;
using Kucoin.Net; using Kucoin.Net;
@@ -20,6 +17,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OKX.Net;
namespace BlazorClient namespace BlazorClient
{ {
@@ -43,38 +41,23 @@ namespace BlazorClient
services.AddServerSideBlazor(); services.AddServerSideBlazor();
// Register the clients, options can be provided in the callback parameter // Register the clients, options can be provided in the callback parameter
services.AddBinance((restClientOptions, socketClientOptions) => { services.AddBinance(restOptions =>
restClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
restClientOptions.LogLevel = LogLevel.Trace;
// Point the logging to use the ILogger configuration, which uses Serilog here
restClientOptions.LogWriters = new List<ILogger> { _loggerFactory.CreateLogger<IBinanceClient>() };
socketClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
});
BinanceClient.SetDefaultOptions(new BinanceClientOptions
{ {
ApiCredentials = new ApiCredentials("KEY", "SECRET"), restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
LogLevel = LogLevel.Trace }, socketOptions =>
});
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions
{ {
ApiCredentials = new ApiCredentials("KEY", "SECRET"), socketOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
}); });
services.AddTransient<IBinanceClient, BinanceClient>();
services.AddScoped<IBinanceSocketClient, BinanceSocketClient>();
services.AddBitfinex(); services.AddBitfinex();
services.AddBitget();
services.AddBittrex(); services.AddBittrex();
services.AddBybit(); services.AddBybit();
services.AddCoinEx(); services.AddCoinEx();
services.AddFTX();
services.AddHuobi(); services.AddHuobi();
services.AddKraken(); services.AddKraken();
services.AddKucoin(); services.AddKucoin();
services.AddOKX();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+2 -1
View File
@@ -10,10 +10,11 @@
@using BlazorClient.Shared @using BlazorClient.Shared
@using Binance.Net.Interfaces.Clients; @using Binance.Net.Interfaces.Clients;
@using Bitfinex.Net.Interfaces.Clients; @using Bitfinex.Net.Interfaces.Clients;
@using Bitget.Net.Interfaces.Clients;
@using Bittrex.Net.Interfaces.Clients; @using Bittrex.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients; @using Bybit.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients; @using CoinEx.Net.Interfaces.Clients;
@using FTX.Net.Interfaces.Clients;
@using Huobi.Net.Interfaces.Clients; @using Huobi.Net.Interfaces.Clients;
@using Kraken.Net.Interfaces.Clients; @using Kraken.Net.Interfaces.Clients;
@using Kucoin.Net.Interfaces.Clients; @using Kucoin.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
+10 -9
View File
@@ -6,15 +6,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="8.0.6" /> <PackageReference Include="Binance.Net" Version="9.1.5" />
<PackageReference Include="Bitfinex.Net" Version="5.0.3" /> <PackageReference Include="Bitfinex.Net" Version="7.0.4" />
<PackageReference Include="Bittrex.Net" Version="7.0.4" /> <PackageReference Include="Bittrex.Net" Version="8.0.3" />
<PackageReference Include="Bybit.Net" Version="0.0.4" /> <PackageReference Include="Bybit.Net" Version="3.2.1" />
<PackageReference Include="CoinEx.Net" Version="5.0.3" /> <PackageReference Include="CoinEx.Net" Version="6.0.3" />
<PackageReference Include="FTX.Net" Version="1.0.4" /> <PackageReference Include="Huobi.Net" Version="5.0.3" />
<PackageReference Include="Huobi.Net" Version="4.0.4" /> <PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" /> <PackageReference Include="JK.OKX.Net" Version="1.4.2" />
<PackageReference Include="Kucoin.Net" Version="4.0.3" /> <PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -17,21 +17,21 @@ namespace ConsoleClient.Exchanges
public async Task<WebCallResult> CancelOrder(string symbol, string id) public async Task<WebCallResult> CancelOrder(string symbol, string id)
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id)); var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id));
return result.AsDataless(); return result.AsDataless();
} }
public async Task<Dictionary<string, decimal>> GetBalances() public async Task<Dictionary<string, decimal>> GetBalances()
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Account.GetAccountInfoAsync(); var result = await client.SpotApi.Account.GetAccountInfoAsync();
return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total); return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total);
} }
public async Task<IEnumerable<OpenOrder>> GetOpenOrders() public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Trading.GetOpenOrdersAsync(); var result = await client.SpotApi.Trading.GetOpenOrdersAsync();
// Should check result success status here // Should check result success status here
return result.Data.Select(o => new OpenOrder return result.Data.Select(o => new OpenOrder
@@ -49,7 +49,7 @@ namespace ConsoleClient.Exchanges
public async Task<decimal> GetPrice(string symbol) public async Task<decimal> GetPrice(string symbol)
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol); var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
// Should check result success status here // Should check result success status here
return result.Data.Price; return result.Data.Price;
@@ -57,7 +57,7 @@ namespace ConsoleClient.Exchanges
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price) public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
{ {
using var client = new BinanceClient(); using var client = new BinanceRestClient();
var result = await client.SpotApi.Trading.PlaceOrderAsync( var result = await client.SpotApi.Trading.PlaceOrderAsync(
symbol, symbol,
side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell, side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell,
@@ -70,7 +70,7 @@ namespace ConsoleClient.Exchanges
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler) public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
{ {
var sub = await _socketClient.SpotStreams.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice)); var sub = await _socketClient.SpotApi.ExchangeData.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
return sub.Data; return sub.Data;
} }
} }
@@ -0,0 +1,75 @@
using Bybit.Net.Clients;
using Bybit.Net.Interfaces.Clients;
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClient.Exchanges
{
internal class BybitExchange : IExchange
{
private IBybitSocketClient _socketClient = new BybitSocketClient();
public async Task<WebCallResult> CancelOrder(string symbol, string id)
{
using var client = new BybitRestClient();
var result = await client.V5Api.Trading.CancelOrderAsync(Bybit.Net.Enums.Category.Spot, symbol, id);
return result.AsDataless();
}
public async Task<Dictionary<string, decimal>> GetBalances()
{
using var client = new BybitRestClient();
var result = await client.V5Api.Account.GetBalancesAsync(Bybit.Net.Enums.AccountType.Spot);
return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance);
}
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
{
using var client = new BybitRestClient();
var order = await client.V5Api.Trading.GetOrdersAsync(Bybit.Net.Enums.Category.Spot);
return order.Data.List.Select(o => new OpenOrder
{
Symbol = o.Symbol,
OrderSide = o.Side.ToString(),
OrderStatus = o.Status.ToString(),
OrderTime = o.CreateTime,
OrderType = o.OrderType.ToString(),
Price = o.Price ?? 0,
Quantity = o.Quantity,
QuantityFilled = o.QuantityFilled ?? 0
});
}
public async Task<decimal> GetPrice(string symbol)
{
using var client = new BybitRestClient();
var result = await client.V5Api.ExchangeData.GetSpotTickersAsync(symbol);
return result.Data.List.First().LastPrice;
}
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
{
using var client = new BybitRestClient();
var result = await client.V5Api.Trading.PlaceOrderAsync(
Bybit.Net.Enums.Category.Spot,
symbol,
side.ToLower() == "buy" ? Bybit.Net.Enums.OrderSide.Buy : Bybit.Net.Enums.OrderSide.Sell,
type == "market" ? Bybit.Net.Enums.NewOrderType.Market : Bybit.Net.Enums.NewOrderType.Limit,
quantity,
price: price);
return result.As(result.Data?.OrderId.ToString());
}
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
{
var sub = await _socketClient.V5SpotApi.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
return sub.Data;
}
}
}
@@ -1,74 +0,0 @@
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using FTX.Net.Clients;
using FTX.Net.Interfaces.Clients;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClient.Exchanges
{
internal class FTXExchange : IExchange
{
private IFTXSocketClient _socketClient = new FTXSocketClient();
public async Task<WebCallResult> CancelOrder(string symbol, string id)
{
using var client = new FTXClient();
var result = await client.TradeApi.Trading.CancelOrderAsync(long.Parse(id));
return result.AsDataless();
}
public async Task<Dictionary<string, decimal>> GetBalances()
{
using var client = new FTXClient();
var result = await client.TradeApi.Account.GetBalancesAsync();
return result.Data.ToDictionary(d => d.Asset, d => d.Total);
}
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
{
using var client = new FTXClient();
var order = await client.TradeApi.Trading.GetOpenOrdersAsync();
return order.Data.Select(o => new OpenOrder
{
Symbol = o.Symbol,
OrderSide = o.Side.ToString(),
OrderStatus = o.Status.ToString(),
OrderTime = o.CreateTime,
OrderType = o.Type.ToString(),
Price = o.Price ?? 0,
Quantity = o.Quantity,
QuantityFilled = o.QuantityFilled ?? 0
});
}
public async Task<decimal> GetPrice(string symbol)
{
using var client = new FTXClient();
var result = await client.TradeApi.ExchangeData.GetSymbolAsync(symbol);
return result.Data.LastPrice ?? 0;
}
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
{
using var client = new FTXClient();
var result = await client.TradeApi.Trading.PlaceOrderAsync(
symbol,
side.ToLower() == "buy" ? FTX.Net.Enums.OrderSide.Buy : FTX.Net.Enums.OrderSide.Sell,
type == "market" ? FTX.Net.Enums.OrderType.Market : FTX.Net.Enums.OrderType.Limit,
quantity,
price: price);
return result.As(result.Data?.Id.ToString());
}
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
{
var sub = await _socketClient.Streams.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice ?? 0));
return sub.Data;
}
}
}
+6 -10
View File
@@ -5,12 +5,10 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Binance.Net.Clients; using Binance.Net.Clients;
using Binance.Net.Objects; using Binance.Net.Objects;
using Bybit.Net.Clients;
using ConsoleClient.Exchanges; using ConsoleClient.Exchanges;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using FTX.Net.Clients;
using FTX.Net.Objects;
using Microsoft.Extensions.Logging;
namespace ConsoleClient namespace ConsoleClient
{ {
@@ -19,20 +17,18 @@ namespace ConsoleClient
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange> static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
{ {
{ "Binance", new BinanceExchange() }, { "Binance", new BinanceExchange() },
{ "FTX", new FTXExchange() } { "Bybit", new BybitExchange() }
}; };
static async Task Main(string[] args) static async Task Main(string[] args)
{ {
BinanceClient.SetDefaultOptions(new BinanceClientOptions BinanceRestClient.SetDefaultOptions(options =>
{ {
LogLevel = LogLevel.Trace, options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
}); });
FTXClient.SetDefaultOptions(new FTXClientOptions BybitRestClient.SetDefaultOptions(options =>
{ {
LogLevel = LogLevel.Trace, options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
}); });
while (true) while (true)
+118 -7
View File
@@ -1,5 +1,5 @@
# CryptoExchange.Net # CryptoExchange.Net
[![.NET](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) ![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg) ![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg) [![.NET](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net)
CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections. CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections.
@@ -8,16 +8,127 @@ CryptoExchange.Net is a base package which can be used to easily implement crypt
## Discord ## Discord
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries. A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
## Donate / Sponsor ## Support the project
I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me. I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS ### Referral link
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2 Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs [Binance](https://accounts.binance.com/en/register?ref=10153680)
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
[Bybit](https://partner.bybit.com/b/jkorf)
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf) ### Donate
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
**Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
**Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
### Sponsor
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 6.2.3 - 02 Dec 2023
* Fixed requestBodyFormat parameter handling
* Version 6.2.2 - 02 Dec 2023
* Added support for specifying the request body content type on a per request basis
* Added DecimalStringWriterConverter
* Added RequestId to WebCallResult model
* Updated response logging
* Version 6.2.1 - 28 Oct 2023
* Utility methods
* Version 6.2.0 - 24 Oct 2023
* Added SerializerOptions helper class for setting a default serializer
* Added ParameterCollection helper class for easier parameter definition
* Added extra helper methods AuthenticationProvider
* Remove interface entries meant for internal use
* Added support for writing int values to the EnumConverter
* Version 6.1.5 - 08 Oct 2023
* Added UpdateType to socket DataEvent
* Added additional scenarios for BoolConverter
* Updated some logging
* Version 6.1.4 - 23 Sep 2023
* Added BoolConverter
* Added parameter for logging warning message on missing enum entry to EnumConverter
* Version 6.1.3 - 18 Sep 2023
* Fix for concurrency exception in socket subscription
* Version 6.1.2 - 11 Sep 2023
* Added support for multiple of the same ratelimiting type in the same rate limiter
* Fixed nullreference on rate limit error if no Retry-After header is returned
* Version 6.1.1 - 04 Sep 2023
* Fixes for json converters
* Version 6.1.0 - 24 Aug 2023
* Added support for ratelimiting on socket connections
* Added rest ratelimit handling and parsing
* Added ServerRatelimitError error
* Version 6.0.3 - 23 Jul 2023
* Fixed Proxy not getting applied in rest clients when not using DI
* Version 6.0.2 - 05 Jul 2023
* Added properties generic dictionary to SocketConnection
* Version 6.0.1 - 29 Jun 2023
* Added LogLevel optional parameter to TraceLoggerProvider
* Version 6.0.0 - 25 Jun 2023
* Updated ApiCredentials to support RSA signing as well as the default Hmac signature
* Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly
* Refactored client options for easier use
* Added easier way of switching environments
* Added ResponseLength and ToString() override on WebCallResult object
* Fixed memory leak in AsyncResetEvent
* Version 5.4.3 - 14 Apr 2023
* Fixed potential threading exception in socket connection
* Version 5.4.2 - 01 Apr 2023
* Reverted socket changes as it seems to cause reconnect to hang
* Version 5.4.1 - 18 Mar 2023
* Added CalculateTradableAmount to SymbolOrderBook
* Improved socket reconnect robustness
* Fixed api rate limiter not working correctly
* Version 5.4.0 - 14 Feb 2023
* Added unsubscribing when receiving subscribe answer after the request timeout has passed
* Fixed socket options copying
* Made TimeSync implementation optional
* Cleaned up ApiCredentials and added better support for extending ApiCredentials
* Version 5.3.1 - 08 Dec 2022
* Added default request parameter ordering before applying authentication
* Fixed possible issue where a socket would reconnect when it should close if it was already in reconnecting
* Version 5.3.0 - 14 Nov 2022
* Reworked client architecture, shifting funcationality to the ApiClient
* Fixed ArrayConverter exponent parsing
* Fixed ArrayConverter not checking null
* Added optional delay setting after establishing socket connection
* Added callback for revitalizing a socket request when reconnecting
* Fixed proxy setting websocket
* Version 5.2.4 - 31 Jul 2022
* Added handling of PlatformNotSupportedException when trying to use websocket from WebAssembly
* Changed DataEvent to have a public constructor for testing purposes
* Fixed EnumConverter serializing values without proper quotes
* Fixed websocket connection reconnecting too quickly when resubscribing/reauthenticating fails
* Version 5.2.3 - 19 Jul 2022
* Fixed socket getting disconnected when `no data` timeout is reached instead of being reconnected
* Version 5.2.2 - 17 Jul 2022 * Version 5.2.2 - 17 Jul 2022
* Added support for retrieving a new url when socket connection is lost and reconnection will happen * Added support for retrieving a new url when socket connection is lost and reconnection will happen
+21 -41
View File
@@ -5,12 +5,12 @@ nav_order: 2
## How to use the library ## How to use the library
Each implementation generally provides two different clients, which will be the access point for the API's. First of the rest client, which is typically available via [ExchangeName]Client, and a socket client, which is generally named [ExchangeName]SocketClient. For example `BinanceClient` and `BinanceSocketClient`. Each implementation generally provides two different clients, which will be the access point for the API's. First of is the rest client, which is typically available via [ExchangeName]RestClient, and a socket client, which is generally named [ExchangeName]SocketClient. For example `BinanceRestClient` and `BinanceSocketClient`.
## Rest client ## Rest client
The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this: The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this:
- KucoinClient - [ExchangeName]RestClient
- SpotApi - SpotApi
- Account - Account
- ExchangeData - ExchangeData
@@ -21,6 +21,7 @@ The rest client gives access to the Rest endpoint of the API. Rest endpoints are
- Trading - Trading
This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints. This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints.
*Requesting ticker info on the spot API* *Requesting ticker info on the spot API*
```csharp ```csharp
var client = new KucoinClient(); var client = new KucoinClient();
@@ -30,7 +31,7 @@ var tickersResult = kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
Structuring the client like this should make it easier to find endpoints and allows for separate options and functionality for different API clients. For example, some API's have totally separate API's for futures, with different base addresses and different API credentials, while other API's have implemented this in the same API. Either way, this structure can facilitate a similar interface. Structuring the client like this should make it easier to find endpoints and allows for separate options and functionality for different API clients. For example, some API's have totally separate API's for futures, with different base addresses and different API credentials, while other API's have implemented this in the same API. Either way, this structure can facilitate a similar interface.
### Rest API client ### Rest API client
The Api clients are parts of the total API with a common identifier. In the previous Kucoin example, it separates the Spot and the Futures API. This again is then separated into topics. Most Rest clients implement the following structure: The Api clients are parts of the total API with a common identifier. In the previous example, it separates the Spot and the Futures API. This again is then separated into topics. Most Rest clients implement the following structure:
**Account** **Account**
Endpoints related to the user account. This can for example be endpoints for accessing account settings, or getting account balances. The endpoints in this topic will require API credentials to be provided in the client options. Endpoints related to the user account. This can for example be endpoints for accessing account settings, or getting account balances. The endpoints in this topic will require API credentials to be provided in the client options.
@@ -44,13 +45,19 @@ Endpoints related to trading. These are endpoints for placing and retrieving ord
### Processing request responses ### Processing request responses
Each request will return a WebCallResult<T> with the following properties: Each request will return a WebCallResult<T> with the following properties:
`RequestHeaders`: The headers send to the server in the request message
`RequestMethod`: The Http method of the request
`RequestUrl`: The url the request was send to
`ResponseLength`: The length in bytes of the response message
`ResponseTime`: The duration between sending the request and receiving the response
`ResponseHeaders`: The headers returned from the server `ResponseHeaders`: The headers returned from the server
`ResponseStatusCode`: The status code as returned by the server `ResponseStatusCode`: The status code as returned by the server
`Success`: Whether or not the call was successful. If successful the `Data` property will contain the resulting data, if not successful the `Error` property will contain more details about what the issue was `Success`: Whether or not the call was successful. If successful the `Data` property will contain the resulting data, if not successful the `Error` property will contain more details about what the issue was
`Error`: Details on what went wrong with a call. Only filled when `Success` == `false` `Error`: Details on what went wrong with a call. Only filled when `Success` == `false`
`Data`: Data returned by the server `OriginalData`: Will contain the originally received unparsed data if this has been enabled in the client options
`Data`: Data returned by the server, only available if `Success` == `true`
When processing the result of a call it should always be checked for success. Not doing so will result in `NullReference` exceptions. When processing the result of a call it should always be checked for success. Not doing so will result in `NullReference` exceptions when the call fails for whatever reason.
*Check call result* *Check call result*
```csharp ```csharp
@@ -65,7 +72,7 @@ Console.WriteLine("Result: " + callResult.Data);
``` ```
## Socket client ## Socket client
The socket client gives access to the websocket API of an exchange. Websocket API's offer streams to which updates are pushed to which a client can listen. Some exchanges also offer some degree of functionality by allowing clients to give commands via the websocket, but most exchanges only allow this via the Rest API. The socket client gives access to the websocket API of an exchange. Websocket API's offer streams to which updates are pushed to which a client can listen, and sometimes also allow request/response communication.
Just like the Rest client is divided in Rest Api clients, the Socket client is divided into Socket Api clients, each with their own range of API functionality. Socket Api clients are generally not divided into topics since the number of methods isn't as big as with the Rest client. To use the Kucoin client as example again, it looks like this: Just like the Rest client is divided in Rest Api clients, the Socket client is divided into Socket Api clients, each with their own range of API functionality. Socket Api clients are generally not divided into topics since the number of methods isn't as big as with the Rest client. To use the Kucoin client as example again, it looks like this:
```csharp ```csharp
@@ -80,7 +87,7 @@ Just like the Rest client is divided in Rest Api clients, the Socket client is d
var subscribeResult = kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler); var subscribeResult = kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
``` ```
Subscribe methods require a data handler parameter, which is the method which will be called when an update is received from the server. This can be the name of a method or a lambda expression. Subscribe methods always require a data handler parameter, which is the method which will be called when an update is received from the server. This can be the name of a method or a lambda expression.
*Method reference* *Method reference*
```csharp ```csharp
@@ -100,12 +107,16 @@ await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData
}); });
``` ```
All updates are wrapped in a `DataEvent<>` object, which contain a `Timestamp`, `OriginalData`, `Topic`, and a `Data` property. The `Timestamp` is the timestamp when the data was received (not send!). `OriginalData` will contain the originally received data if this has been enabled in the client options. `Topic` will contain the topic of the update, which is typically the symbol or asset the update is for. The `Data` property contains the received update data. All updates are wrapped in a `DataEvent<>` object, which contain the following properties:
`Timestamp`: The timestamp when the data was received (not send!)
`OriginalData`: Will contain the originally received unparsed data if this has been enabled in the client options
`Topic`: Will contain the topic of the update, which is typically the symbol or asset the update is for
`Data`: Contains the received update data.
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.* *[WARNING] Do not use `using` statements in combination with constructing a `SocketClient` without blocking the thread. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
### Processing subscribe responses ### Processing subscribe responses
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same was as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection. Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as a [rest request](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
```csharp ```csharp
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler); var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
@@ -158,34 +169,3 @@ await kucoinSocketClient.UnsubscribeAsync(subscriptionResult.Data.Id);
When you need to unsubscribe all current subscriptions on a client you can call `UnsubscribeAllAsync` on the client to unsubscribe all streams and close all connections. When you need to unsubscribe all current subscriptions on a client you can call `UnsubscribeAllAsync` on the client to unsubscribe all streams and close all connections.
## Dependency injection
Each library offers a `Add[Library]` extension method for `IServiceCollection`, which allows you to add the clients to the service collection. It also provides a callback for setting the client options. See this example for adding the `BinanceClient`:
```csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddBinance((restClientOptions, socketClientOptions) => {
restClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
restClientOptions.LogLevel = LogLevel.Trace;
socketClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
});
}
```
Doing client registration this way will add the `IBinanceClient` as a transient service, and the `IBinanceSocketClient` as a scoped service.
Alternatively, the clients can be registered manually:
```csharp
BinanceClient.SetDefaultOptions(new BinanceClientOptions
{
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
LogLevel = LogLevel.Trace
});
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions
{
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
});
services.AddTransient<IBinanceClient, BinanceClient>();
services.AddScoped<IBinanceSocketClient, BinanceSocketClient>();
```
+4 -11
View File
@@ -1,6 +1,6 @@
--- ---
title: FAQ title: FAQ
nav_order: 11 nav_order: 12
--- ---
## Frequently asked questions ## Frequently asked questions
@@ -48,18 +48,11 @@ private void SomeMethod()
``` ```
### Can I use the TestNet/US/other API with this library ### Can I use the TestNet/US/other API with this library
Yes, generally these are all supported and can be configured by setting the BaseAddress in the client options. Some known API addresses should be available in the [Exchange]ApiAddresses class. For example: Yes, generally these are all supported and can be configured by setting the Environment in the client options. Some known environments should be available in the [Exchange]Environment class. For example:
```csharp ```csharp
var client = new BinanceClient(new BinanceClientOptions var client = new BinanceRestClient(options =>
{ {
SpotApiOptions = new BinanceApiClientOptions options.Environment = BinanceEnvironment.Testnet;
{
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
},
UsdFuturesApiOptions = new BinanceApiClientOptions
{
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress
}
}); });
``` ```
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
title: Glossary title: Glossary
nav_order: 10 nav_order: 11
--- ---
## Terms and definitions ## Terms and definitions
@@ -18,7 +18,7 @@ nav_order: 10
|Network|Chain|The network of an asset. For example `ETH` allows multiple networks like `ERC20` and `BEP2`| |Network|Chain|The network of an asset. For example `ETH` allows multiple networks like `ERC20` and `BEP2`|
|Order book|Market depth|A list of (the top rows of) the current best bids and asks| |Order book|Market depth|A list of (the top rows of) the current best bids and asks|
|Ticker|Stats|Statistics over the last 24 hours| |Ticker|Stats|Statistics over the last 24 hours|
|Client implementation|Library|An implementation of the `CrytpoExchange.Net` library. For example `Binance.Net` or `FTX.Net`| |Client implementation|Library|An implementation of the `CrytpoExchange.Net` library. For example `Binance.Net` or `Bybit.Net`|
### Other naming conventions ### Other naming conventions
#### PlaceOrderAsync #### PlaceOrderAsync
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
title: Common interfaces title: Common interfaces
nav_order: 5 nav_order: 7
--- ---
## Shared interfaces ## Shared interfaces
+95 -270
View File
@@ -1,320 +1,114 @@
--- ---
title: Log config title: Logging
nav_order: 4 nav_order: 5
--- ---
## Configuring logging ## Configuring logging
The library offers extensive logging, for which you can supply your own logging implementation. The logging can be configured via the client options (see [Client options](https://github.com/JKorf/CryptoExchange.Net/wiki/Options)). The examples here are using the `BinanceClient` but they should be the same for each implementation. The library offers extensive logging, which depends on the dotnet `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when connecting the library logging to your existing logging implementation.
Logging is based on the `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when connecting the library logging to your existing logging implementation. *Configure logging to write to the console*
## Serilog
To make the CryptoExchange.Net logging write to the Serilog logger you can use the following methods, depending on the type of project you're using. The following examples assume that the `Serilog.Sinks.Console` package is already installed.
### Dotnet hosting
With for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client. Make sure to install the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore).
<Details>
<Summary>
Using ILogger injection
</Summary>
<BlockQuote>
Adding `UseSerilog()` in the `CreateHostBuilder` will add the Serilog logging implementation as an ILogger which you can inject into implementations.
*Configuring Serilog as ILogger:*
```csharp ```csharp
IServiceCollection services = new ServiceCollection();
public static void Main(string[] args) services
.AddBinance()
.AddLogging(options =>
{ {
options.SetMinimumLevel(LogLevel.Trace);
options.AddConsole();
});
```
The library provides a TraceLogger ILogger implementation which writes log messages using `Trace.WriteLine`, but any other logging library can be used.
*Configure logging to use trace logging*
```csharp
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddBinance()
.AddLogging(options =>
{
options.SetMinimumLevel(LogLevel.Trace);
options.AddProvider(new TraceLoggerProvider());
});
```
### Using an external logging library and dotnet DI
With for example an ASP.Net Core or Blazor project the logging can be configured by the dependency container, which can then automatically be used be the clients.
The next example shows how to use Serilog. This assumes the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore) is installed.
*Using serilog:*
```csharp
using Binance.Net;
using Serilog;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() .MinimumLevel.Debug()
.WriteTo.Console() .WriteTo.Console()
.CreateLogger(); .CreateLogger();
CreateHostBuilder(args).Build().Run(); var builder = WebApplication.CreateBuilder(args);
} builder.Services.AddBinance();
builder.Host.UseSerilog();
var app = builder.Build();
public static IHostBuilder CreateHostBuilder(string[] args) => // startup
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
app.Run();
``` ```
### Logging without dotnet DI
If you don't have a dependency injection service available because you are for example working on a simple console application you have 2 options for logging.
#### Create a ServiceCollection manually and get the client from the service provider
*Injecting ILogger:*
```csharp ```csharp
IServiceCollection serviceCollection = new ServiceCollection();
public class BinanceDataProvider serviceCollection.AddBinance();
serviceCollection.AddLogging(options =>
{ {
BinanceClient _client; options.SetMinimumLevel(LogLevel.Trace);
options.AddConsole();
}).BuildServiceProvider();
public BinanceDataProvider(ILogger<BinanceDataProvider> logger) var client = serviceCollection.GetRequiredService<IBinanceRestClient>();
{
_client = new BinanceClient(new BinanceClientOptions
{
LogLevel = LogLevel.Trace,
LogWriters = new List<ILogger> { logger }
});
}
}
``` ```
</BlockQuote> #### Create a LoggerFactory manually
</Details>
<Details>
<Summary>
Using Add[Library] extension method
</Summary>
<BlockQuote>
When using the `Add[Library]` extension method, for instance `AddBinance()`, there is a small issue that there is no available `ILogger<>` yet when adding the library. This can be solved as follows:
*Configuring Serilog as ILogger:*
```csharp ```csharp
var logFactory = new LoggerFactory();
public static void Main(string[] args) logFactory.AddProvider(new ConsoleLoggerProvider());
{ var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options => { });
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup(
context => new Startup(context.Configuration, LoggerFactory.Create(config => config.AddSerilog()) )); // <- this allows us to use ILoggerFactory in the Startup.cs
});
``` ```
## Providing logging for issues
*Injecting ILogger:*
```csharp
public class Startup
{
private ILoggerFactory _loggerFactory;
public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
{
Configuration = configuration;
_loggerFactory = loggerFactory;
}
/* .. rest of class .. */
public void ConfigureServices(IServiceCollection services)
{
services.AddBinance((restClientOptions, socketClientOptions) => {
// Point the logging to use the ILogger configuration
restClientOptions.LogWriters = new List<ILogger> { _loggerFactory.CreateLogger<IBinanceClient>() };
});
// Rest of service registrations
}
}
```
</BlockQuote>
</Details>
### Console application
If you don't have a dependency injection service available because you are for example working on a simple console application you can use a slightly different approach.
*Configuring Serilog as ILogger:*
```csharp
var serilogLogger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
var loggerFactory = (ILoggerFactory)new LoggerFactory();
loggerFactory.AddSerilog(serilogLogger);
```
*Injecting ILogger:*
```csharp
var client = new BinanceClient(new BinanceClientOptions
{
LogLevel = LogLevel.Trace,
LogWriters = new List<ILogger> { loggerFactory.CreateLogger("") }
});
```
The `BinanceClient` will now write the logging it produces to the Serilog logger.
## Log4Net
To make the CryptoExchange.Net logging write to the Log4Net logge with for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client you're using. Make sure to install the `Microsoft.Extensions.Logging.Log4Net.AspNetCore` package (https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore).
Adding `AddLog4Net()` in the `ConfigureLogging` call will add the Log4Net implementation as an ILogger which you can inject into implementations. Make sure you have a log4net.config configuration file in your project.
*Configuring Log4Net as ILogger:*
```csharp
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureLogging(logging =>
{
logging.AddLog4Net();
logging.SetMinimumLevel(LogLevel.Trace);
});
webBuilder.UseStartup<Startup>();
});
```
*Injecting ILogger:*
```csharp
public class BinanceDataProvider
{
BinanceClient _client;
public BinanceDataProvider(ILogger<BinanceDataProvider> logger)
{
_client = new BinanceClient(new BinanceClientOptions
{
LogLevel = LogLevel.Trace,
LogWriters = new List<ILogger> { logger }
});
}
}
```
If you don't have the Dotnet dependency container available you'll need to provide your own ILogger implementation. See [Custom logger](#custom-logger).
## NLog
To make the CryptoExchange.Net logging write to the NLog logger you can use the following ways, depending on the type of project you're using.
### Dotnet hosting
With for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client you're using. Make sure to install the `NLog.Web.AspNetCore` package (https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-5).
Adding `UseNLog()` to the `CreateHostBuilder()` method will add the NLog implementation as an ILogger which you can inject into implementations. Make sure you have a nlog.config configuration file in your project.
*Configuring NLog as ILogger:*
```csharp
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Trace);
})
.UseNLog();
```
*Injecting ILogger:*
```csharp
public class BinanceDataProvider
{
BinanceClient _client;
public BinanceDataProvider(ILogger<BinanceDataProvider> logger)
{
_client = new BinanceClient(new BinanceClientOptions
{
LogLevel = LogLevel.Trace,
LogWriters = new List<ILogger> { logger }
});
}
}
```
If you don't have the Dotnet dependency container available you'll need to provide your own ILogger implementation. See [Custom logger](#custom-logger).
## Custom logger
If you're using a different framework or for some other reason these methods don't work for you you can create a custom ILogger implementation to receive the logging. All you need to do is create an implementation of the ILogger interface and provide that to the client.
*A simple console logging implementation (note that the ConsoleLogger is already available in the CryptoExchange.Net library)*:
```csharp
public class ConsoleLogger : ILogger
{
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)
{
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
Console.WriteLine(logMessage);
}
}
```
*Injecting the console logging implementation:*
```csharp
var client = new BinanceClient(new BinanceClientOptions
{
LogLevel = LogLevel.Trace,
LogWriters = new List<ILogger> { new ConsoleLogger() }
});
```
## Provide logging for issues
A big debugging tool when opening an issue on Github is providing logging of what data caused the issue. This can be provided two ways, via the `OriginalData` property of the call result or data event, or collecting the Trace logging. A big debugging tool when opening an issue on Github is providing logging of what data caused the issue. This can be provided two ways, via the `OriginalData` property of the call result or data event, or collecting the Trace logging.
### OriginalData ### OriginalData
This is only useful when there is an issue in deserialization. So either a call result is giving a Deserialization error, or the result has a value that is unexpected. If that is the issue, please provide the original data that is received so the deserialization issue can be resolved based on the received data. This is only useful when there is an issue in deserialization. So either a call result is giving a Deserialization error, or the result has a value that is unexpected. If that is the issue, please provide the original data that is received so the deserialization issue can be resolved based on the received data.
By default the `OriginalData` property in the `WebCallResult`/`DataEvent` object is not filled as saving the original data has a (very small) performance penalty. To save the original data in the `OriginalData` property the `OutputOriginalData` option should be set to `true` in the client options. By default the `OriginalData` property in the `WebCallResult`/`DataEvent` object is not filled as saving the original data has a (very small) performance penalty. To save the original data in the `OriginalData` property the `OutputOriginalData` option should be set to `true` in the client options.
*Enabled output data* *Enabled output data*
```csharp ```csharp
var client = new BinanceClient(new BinanceClientOptions var client = new BinanceClient(options =>
{ {
OutputOriginalData = true options.OutputOriginalData = true
}); });
``` ```
*Accessing original data* *Accessing original data*
```csharp ```csharp
// Rest request // Rest request
var tickerResult = client.SpotApi.ExchangeData.GetTickersAsync(); var tickerResult = await client.SpotApi.ExchangeData.GetTickersAsync();
var originallyRecievedData = tickerResult.OriginalData; var originallyReceivedData = tickerResult.OriginalData;
// Socket update // Socket update
client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => { await client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
var originallyRecievedData = update.OriginalData; var originallyRecievedData = update.OriginalData;
}); });
``` ```
### Trace logging ### Trace logging
Trace logging, which is the most verbose log level, can be enabled in the client options. Trace logging, which is the most verbose log level, will show everything the library does and includes the data that was send and received.
*Enabled output data*
```csharp
var client = new BinanceClient(new BinanceClientOptions
{
LogLevel = LogLevel.Trace
});
```
After enabling trace logging all data send to/received from the server is written to the log writers. By default this is written to the output window in Visual Studio via Debug.WriteLine, though this might be different depending on how you configured your logging.
Output data will look something like this: Output data will look something like this:
``` ```
2021-12-17 10:40:42:296 | Debug | Binance | Client configuration: LogLevel: Trace, Writers: 1, OutputOriginalData: False, Proxy: -, AutoReconnect: True, ReconnectInterval: 00:00:05, MaxReconnectTries: , MaxResubscribeTries: 5, MaxConcurrentResubscriptionsPerSocket: 5, SocketResponseTimeout: 00:00:10, SocketNoDataTimeout: 00:00:00, SocketSubscriptionsCombineTarget: , CryptoExchange.Net: v5.0.0.0, Binance.Net: v8.0.0.0 2021-12-17 10:40:42:296 | Debug | Binance | Client configuration: LogLevel: Trace, Writers: 1, OutputOriginalData: False, Proxy: -, AutoReconnect: True, ReconnectInterval: 00:00:05, MaxReconnectTries: , MaxResubscribeTries: 5, MaxConcurrentResubscriptionsPerSocket: 5, SocketResponseTimeout: 00:00:10, SocketNoDataTimeout: 00:00:00, SocketSubscriptionsCombineTarget: , CryptoExchange.Net: v5.0.0.0, Binance.Net: v8.0.0.0
@@ -323,3 +117,34 @@ Output data will look something like this:
2021-12-17 10:40:43:024 | Debug | Binance | [15] Response received in 571ms: {"symbol":"BTCUSDT","priceChange":"-1726.47000000","priceChangePercent":"-3.531","weightedAvgPrice":"48061.51544204","prevClosePrice":"48901.44000000","lastPrice":"47174.97000000","lastQty":"0.00352000","bidPrice":"47174.96000000","bidQty":"0.65849000","askPrice":"47174.97000000","askQty":"0.13802000","openPrice":"48901.44000000","highPrice":"49436.43000000","lowPrice":"46749.55000000","volume":"33136.69765000","quoteVolume":"1592599905.80360790","openTime":1639647642763,"closeTime":1639734042763,"firstId":1191596486,"lastId":1192649611,"count":1053126} 2021-12-17 10:40:43:024 | Debug | Binance | [15] Response received in 571ms: {"symbol":"BTCUSDT","priceChange":"-1726.47000000","priceChangePercent":"-3.531","weightedAvgPrice":"48061.51544204","prevClosePrice":"48901.44000000","lastPrice":"47174.97000000","lastQty":"0.00352000","bidPrice":"47174.96000000","bidQty":"0.65849000","askPrice":"47174.97000000","askQty":"0.13802000","openPrice":"48901.44000000","highPrice":"49436.43000000","lowPrice":"46749.55000000","volume":"33136.69765000","quoteVolume":"1592599905.80360790","openTime":1639647642763,"closeTime":1639734042763,"firstId":1191596486,"lastId":1192649611,"count":1053126}
``` ```
When opening an issue, please provide this logging when available. When opening an issue, please provide this logging when available.
### Example of serilog config and minimal API's
```csharp
using Binance.Net;
using Binance.Net.Interfaces.Clients;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBinance();
builder.Host.UseSerilog();
var app = builder.Build();
// startup
app.Urls.Add("http://localhost:3000");
app.MapGet("/price/{symbol}", async (string symbol) =>
{
var client = app.Services.GetRequiredService<IBinanceRestClient>();
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
return result.Data.Price;
});
app.Run();
```
+48 -75
View File
@@ -1,100 +1,73 @@
--- ---
title: Migrate v4 to v5 title: Migrate v5 to v6
nav_order: 9 nav_order: 10
--- ---
## Migrating from version 4 to version 5 ## Migrating from version 5 to version 6
When updating your code from version 4 implementations to version 5 implementations you will encounter a fair bit of breaking changes. Here is the general outline for changes made in the CryptoExchange.Net library. For more specific changes for each library visit the library migration guide. When updating your code from version 5 implementations to version 6 implementations you will encounter some breaking changes. Here is the general outline of changes made in the CryptoExchange.Net library. For more specific changes for each library visit the library migration guide.
*NOTE when updating it is not possible to have some client implementations use a V4 version and some clients a V5. When updating all libraries should be migrated* *NOTE when updating it is not possible to have some client implementations use a V5 version and some clients a V6. When updating all libraries should be migrated*
## Client structure ## Rest client name
The client structure has been changed to make clients more consistent across different implementations. Clients using V4 either had `client.Method()`, `client.[Api].Method()` or `client.[Api].[Topic].Method()`. To be more clear about different clients for different API's the rest client implementations have been renamed from [Exchange]Client to [Exchange]RestClient. This makes it more clear that it only implements the Rest API and the [Exchange]SocketClient the Socket API.
This has been unified to be `client.[Api]Api.[Topic].Method()`: ## Options
`bittrexClient.GetTickersAsync()` -> `bittrexClient.SpotApi.ExchangeData.GetTickersAsync()` Option parameters have been changed to a callback instead of an options object. This makes processing of the options easier and is in line with how dotnet handles option configurations.
`kucoinClient.Spot.GetTickersAsync()` -> `kucoinClient.SpotApi.ExchangeData.GetTickersAsync()`
`binanceClient.Spot.Market.GetTickersAsync()` -> `binanceClient.SpotApi.ExchangeData.GetTickersAsync()`
Socket clients are restructured as `client.[Api]Streams.Method()`: **BaseAddress**
`bittrexClient.SpotStreams.SubscribeToTickerUpdatesAsync()` The BaseAddress option has been replaced by the Environment option. The Environment options allows for selection/switching between different trade environments more easily. For example the environment can be switched between a testnet and live by changing only a single line instead of having to change all BaseAddresses.
`kucoinClient.SpotStreams.SubscribeToTickerUpdatesAsync()`
`binanceClient.SpotStreams.SubscribeToAllTickerUpdatesAsync()`
**LogLevel/LogWriters**
The logging options have been removed and are now inherited by the DI configuration. See [Logging](https://jkorf.github.io/CryptoExchange.Net/Logging.html) for more info.
## Options structure **HttpClient**
The options have been changed in 2 categories, options for the whole client, and options only for a specific sub Api. Some options might no longer be available on the base level and should be set on the Api options instead, for example the `BaseAddress`. The HttpClient will now be received by the DI container instead of having to pass it manually. When not using DI it is still possible to provide a HttpClient, but it is now located in the client constructor.
The following example sets some basic options, and specifically overwrites the USD futures Api options to use the test net address and different Api credentials:
*V4*
```csharp
var binanceClient = new BinanceClient(new BinanceApiClientOptions{
LogLevel = LogLevel.Trace,
RequestTimeout = TimeSpan.FromSeconds(60),
ApiCredentials = new ApiCredentials("API KEY", "API SECRET"),
BaseAddressUsdtFutures = new ApiCredentials("OTHER API KEY ONLY FOR USD FUTURES", "OTHER API SECRET ONLY FOR USD FUTURES")
// No way to set separate credentials for the futures API
});
```
*V5* *V5*
```csharp ```csharp
var binanceClient = new BinanceClient(new BinanceClientOptions() var client = new BinanceClient(new BinanceClientOptions(){
{ OutputOriginalData = true,
// Client options SpotApiOptions = new RestApiOptions {
LogLevel = LogLevel.Trace, BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
RequestTimeout = TimeSpan.FromSeconds(60),
ApiCredentials = new ApiCredentials("API KEY", "API SECRET"),
// Set options specifically for the USD futures API
UsdFuturesApiOptions = new BinanceApiClientOptions
{
BaseAddress = BinanceApiAddresses.TestNet.UsdFuturesRestClientAddress,
ApiCredentials = new ApiCredentials("OTHER API KEY ONLY FOR USD FUTURES", "OTHER API SECRET ONLY FOR USD FUTURES")
} }
// Other options
}); });
``` ```
See [Client options](https://github.com/JKorf/CryptoExchange.Net/wiki/Options) for more details on the specific options.
## IExchangeClient *V6*
The `IExchangeClient` has been replaced by the `ISpotClient` and `IFuturesClient`. Where previously the `IExchangeClient` was implemented on the base client level, the `ISpotClient`/`IFuturesClient` have been implemented on the sub-Api level.
This, in combination with the client restructuring, allows for more logically implemented interfaces, see this example:
*V4*
```csharp ```csharp
var spotClients = new [] { var client = new BinanceClient(options => {
(IExhangeClient)binanceClient, options.OutputOriginalData = true;
(IExchangeClient)bittrexClient, options.Environment = BinanceEnvironment.Testnet;
(IExchangeClient)kucoinClient.Spot // Other options
}; });
// There was no common implementation for futures client
``` ```
## Socket api name
As socket API's are often more than just streams to subscribe to the name of the socket API clients have been changed from [Topic]Streams to [Topic]Api which matches the rest API client names. For example `SpotStreams` has become `SpotApi`, so `binanceSocketClient.UsdFuturesStreams.SubscribeXXX` has become `binanceSocketClient.UsdFuturesApi.SubscribeXXX`.
## Add[Exchange] extension method
With the change in options providing the DI extension methods for the IServiceCollection have also been changed slightly. Also the socket clients will now be registered as Singleton by default instead of Scoped.
*V5* *V5*
```csharp ```csharp
var spotClients = new [] { builder.Services.AddKucoin((restOpts, socketOpts) =>
binanceClient.SpotApi.CommonSpotClient, {
bittrexClient.SpotApi.CommonSpotClient, restOpts.LogLevel = LogLevel.Debug;
kucoinClient.SpotApi.CommonSpotClient restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
}; socketOpts.LogLevel = LogLevel.Debug;
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
var futuresClients = new [] { }, ServiceLifetime.Singleton);
binanceClient.UsdFuturesApi.CommonFuturesClient,
kucoinClient.FuturesApi.CommonFuturesClient
};
``` ```
Where the IExchangeClient was returning interfaces which were implemented by models from the exchange, the `ISpotClient`/`IFuturesClient` returns actual objects defined in the `CryptoExchange.Net` library. This shifts the responsibility of parsing *V6*
the library model to a shared model from the model class to the client class, which makes more sense and removes the need for separate library models to implement the same mapping logic. It also removes the need for the `Common` prefix on properties:
*V4*
```csharp ```csharp
var kline = await ((IExhangeClient)binanceClient).GetKlinesAysnc(/*params*/); builder.Services.AddKucoin((restOpts) =>
var closePrice = kline.CommonClose; {
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
},
(socketOpts) =>
{
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
});
``` ```
*V5*
```csharp
var kline = await binanceClient.SpotApi.ComonSpotClient.GetKlinesAysnc(/*params*/);
var closePrice = kline.ClosePrice;
```
For more details on the interfaces see [Common interfaces](interfaces.html)
+47 -36
View File
@@ -1,6 +1,6 @@
--- ---
title: Client options title: Client options
nav_order: 3 nav_order: 4
--- ---
## Setting client options ## Setting client options
@@ -10,10 +10,12 @@ Each implementation can be configured using client options. There are 2 ways to
*Set the default options to use for new clients* *Set the default options to use for new clients*
```csharp ```csharp
BinanceClient.SetDefaultOptions(new BinanceClientOptions BinanceClient.SetDefaultOptions(options =>
{ {
LogLevel = LogLevel.Trace, options.OutputOriginalData = true;
ApiCredentials = new ApiCredentials("KEY", "SECRET") options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
// Override the api credentials for the Spot API
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
}); });
``` ```
@@ -21,10 +23,12 @@ BinanceClient.SetDefaultOptions(new BinanceClientOptions
*Set the options to use for a single new client* *Set the options to use for a single new client*
```csharp ```csharp
var client = new BinanceClient(new BinanceClientOptions var client = new BinanceClient(options =>
{ {
LogLevel = LogLevel.Trace, options.OutputOriginalData = true;
ApiCredentials = new ApiCredentials("KEY", "SECRET") options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
// Override the api credentials for the Spot API
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
}); });
``` ```
@@ -32,39 +36,30 @@ var client = new BinanceClient(new BinanceClientOptions
When calling `SetDefaultOptions` each client created after that will use the options that were set, unless the specific option is overriden in the options that were provided to the client. Consider the following example: When calling `SetDefaultOptions` each client created after that will use the options that were set, unless the specific option is overriden in the options that were provided to the client. Consider the following example:
```csharp ```csharp
BinanceClient.SetDefaultOptions(new BinanceClientOptions BinanceClient.SetDefaultOptions(options =>
{ {
LogLevel = LogLevel.Trace, options.OutputOriginalData = true;
OutputOriginalData = true
}); });
var client = new BinanceClient(new BinanceClientOptions var client = new BinanceClient(options =>
{ {
LogLevel = LogLevel.Debug, options.OutputOriginalData = false;
ApiCredentials = new ApiCredentials("KEY", "SECRET")
}); });
``` ```
The client instance will have the following options: The client instance will have the following options:
`LogLevel = Debug` `OutputOriginalData = false`
`OutputOriginalData = true`
`ApiCredentials = set`
## Api options ## Api options
The options are divided in two categories. The basic options, which will apply to everything the client does, and the Api options, which is limited to the specific API client (see [Clients](https://github.com/JKorf/CryptoExchange.Net/wiki/Clients)). The options are divided in two categories. The basic options, which will apply to everything the client does, and the Api options, which is limited to the specific API client (see [Clients](https://jkorf.github.io/CryptoExchange.Net/Clients.html)).
```csharp ```csharp
var client = new BinanceClient(new BinanceClientOptions var client = new BinanceRestClient(options =>
{ {
LogLevel = LogLevel.Debug, options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"), options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
SpotApiOptions = new BinanceApiClientOptions
{
ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET") ,
BaseAddress = BinanceApiAddresses.Us.RestClientAddress
}
}); });
``` ```
@@ -78,38 +73,39 @@ All clients have access to the following options, specific implementations might
|Option|Description|Default| |Option|Description|Default|
|------|-----------|-------| |------|-----------|-------|
|`LogWriters`| A list of `ILogger`s to handle log messages. | `new List<ILogger> { new DebugLogger() }` |
|`LogLevel`| The minimum log level before passing messages to the `LogWriters`. Messages with a more verbose level than the one specified here will be ignored. Setting this to `null` will pass all messages to the `LogWriters`.| `LogLevel.Information`
|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update. | `false` |`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update. | `false`
|`ApiCredentials`| The API credentials to use for accessing protected endpoints. Typically a key/secret combination. Note that this is a `default` value for all API clients, and can be overridden per API client. See the `Base Api client options`| `null` |`ApiCredentials`| The API credentials to use for accessing protected endpoints. Can either be an API key/secret using Hmac encryption or an API key/private key using RSA encryption for exchanges that support that. See [Credentials](#credentials). Note that this is a `default` value for all API clients, and can be overridden per API client. See the `Base Api client options`| `null`
|`Proxy`|The proxy to use for connecting to the API.| `null` |`Proxy`|The proxy to use for connecting to the API.| `null`
|`RequestTimeout`|The timeout for client requests to the server| `TimeSpan.FromSeconds(20)`
**Rest client options (extension of base client options)** **Rest client options (extension of base client options)**
|Option|Description|Default| |Option|Description|Default|
|------|-----------|-------| |------|-----------|-------|
|`RequestTimeout`|The time out to use for requests.|`TimeSpan.FromSeconds(30)`| |`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time sure is to be in sync.|`true`|
|`HttpClient`|The `HttpClient` instance to use for making requests. When creating multiple `RestClient` instances a single `HttpClient` should be provided to prevent each client instance from creating its own. *[WARNING] When providing the `HttpClient` instance in the options both the `RequestTimeout` and `Proxy` client options will be ignored and should be set on the provided `HttpClient` instance.*| `null` | |`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed| `TimeSpan.FromHours(1)`
|`Environment`|The environment the library should talk to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments|`Live environment`
**Socket client options (extension of base client options)** **Socket client options (extension of base client options)**
|Option|Description|Default| |Option|Description|Default|
|------|-----------|-------| |------|-----------|-------|
|`AutoReconnect`|Whether or not the socket should automatically reconnect when disconnected.|`true` |`AutoReconnect`|Whether or not the socket should attempt to automatically reconnect when disconnected.|`true`
|`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)` |`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)`
|`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)` |`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)`
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout) |`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout)
|`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges 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.| Depends on implementation |`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges 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.| Depends on implementation
|`MaxReconnectTries`|The maximum amount of tries for reconnecting|`null` (infinite)
|`MaxResubscribeTries`|The maximum amount of tries for resubscribing after successfully reconnecting the socket|5
|`MaxConcurrentResubscriptionsPerSocket`|The maximum number of concurrent resubscriptions per socket when resubscribing after reconnecting|5 |`MaxConcurrentResubscriptionsPerSocket`|The maximum number of concurrent resubscriptions per socket when resubscribing after reconnecting|5
|`MaxSocketConnections`|The maximum amount of distinct socket connections|`null`
|`DelayAfterConnect`|The time to wait before sending messages after connecting to the server.|`TimeSpan.Zero`
|`Environment`|The environment the library should talk to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments|`Live environment`
**Base Api client options** **Base Api client options**
|Option|Description|Default| |Option|Description|Default|
|------|-----------|-------| |------|-----------|-------|
|`ApiCredentials`|The API credentials to use for this specific API client. Will override any credentials provided in the base client options| |`ApiCredentials`|The API credentials to use for accessing protected endpoints. Can either be an API key/secret using Hmac encryption or an API key/private key using RSA encryption for exchanges that support that. See [Credentials](#credentials). Setting ApiCredentials on the Api Options will override any default ApiCredentials on the `Base client options`| `null`
|`BaseAddress`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example. Available base addresses are defined in the [Library]ApiAddresses helper class, for example `KucoinApiAddresses`|Depends on implementation |`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update.|False
**Options for Rest Api Client (extension of base api client options)** **Options for Rest Api Client (extension of base api client options)**
@@ -117,6 +113,21 @@ All clients have access to the following options, specific implementations might
|------|-----------|-------| |------|-----------|-------|
|`RateLimiters`|A list of `IRateLimiter`s to use.|`new List<IRateLimiter>()`| |`RateLimiters`|A list of `IRateLimiter`s to use.|`new List<IRateLimiter>()`|
|`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`| |`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`|
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time is sure to be in sync. Overrides the Rest client options `AutoTimestamp` option if set|`null`|
|`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed. Overrides the Rest client options `TimestampRecalculationInterval` option if set| `TimeSpan.FromHours(1)`
**Options for Socket Api Client (extension of base api client options)** **Options for Socket Api Client (extension of base api client options)**
There are currently no specific options for socket API clients, the base API options are still available.
|Option|Description|Default|
|------|-----------|-------|
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. Overrides the Socket client options `SocketNoDataTimeout` option if set | `default(TimeSpan)` (no timeout)
|`MaxSocketConnections`|The maximum amount of distinct socket connections. Overrides the Socket client options `MaxSocketConnections` option if set |`null`
## Credentials
Credentials are supported in 3 formats in the base library:
|Type|Description|Example|
|----|-----------|-------|
|`Hmac`|An API key + secret combination. The API key is send with the request and the secret is used to sign requests. This is the default authentication method on all exchanges. |`options.ApiCredentials = new ApiCredentials("51231f76e-9c503548-8fabs3f-rfgf12mkl3", "556be32-d563ba53-faa2dfd-b3n5c", CredentialType.Hmac);`|
|`RsaPem`|An API key + a public and private key pair generated by the user. The public key is shared with the exchange, while the private key is used to sign requests. This CredentialType expects the private key to be in .pem format and is only supported in .netstandard2.1 due to limitations of the framework|`options.ApiCredentials = new ApiCredentials("432vpV8daAaXAF4Qg", ""-----BEGIN PRIVATE KEY-----[PRIVATEKEY]-----END PRIVATE KEY-----", CredentialType.RsaPem);`|
|`RsaXml`|An API key + a public and private key pair generated by the user. The public key is shared with the exchange, while the private key is used to sign requests. This CredentialType expects the private key to be in xml format and is supported in .netstandard2.0 and .netstandard2.1, but it might mean the private key needs to be converted from the original format to xml|`options.ApiCredentials = new ApiCredentials("432vpV8daAaXAF4Qg", "<RSAKeyValue>[PRIVATEKEY]</RSAKeyValue>", CredentialType.RsaXml);`|
+13 -2
View File
@@ -4,11 +4,11 @@ nav_order: 6
--- ---
## Locally synced order book ## Locally synced order book
Each implementation provides an order book implementation. These implementations will provide a client side order book and will take care of synchronization with the server, and will handle reconnecting and resynchronizing in case of a dropped connection. Each exchange implementation provides an order book implementation. These implementations will provide a client side order book and will take care of synchronization with the server, and will handle reconnecting and resynchronizing in case of a dropped connection.
Order book implementations are named as `[ExchangeName][Type]SymbolOrderBook`, for example `BinanceSpotSymbolOrderBook`. Order book implementations are named as `[ExchangeName][Type]SymbolOrderBook`, for example `BinanceSpotSymbolOrderBook`.
## Usage ## Usage
Start the book synchronization by calling the `StartAsync` method. This returns a success state whether the book is successfully synchronized and started. You can listen to the `OnStatusChange` event to be notified of when the status of a book changes. Note that the order book is only synchronized with the server when the state is `Synced`. Start the book synchronization by calling the `StartAsync` method. This returns whether the book is successfully synchronized and started. You can listen to the `OnStatusChange` event to be notified of when the status of a book changes. Note that the order book is only synchronized with the server when the state is `Synced`. When the order book has been started and the state changes from `Synced` to `Reconnecting` the book will automatically reconnect and resync itself.
*Start an order book and print the top 3 rows* *Start an order book and print the top 3 rows*
```csharp ```csharp
@@ -24,6 +24,7 @@ if (!startResult.Success)
while(true) while(true)
{ {
Console.Clear();
Console.WriteLine(book.ToString(3); Console.WriteLine(book.ToString(3);
await Task.Delay(500); await Task.Delay(500);
} }
@@ -55,3 +56,13 @@ book.OnOrderBookUpdate += (bidsAsks) => { Console.WriteLine($"Order book changed
book.OnBestOffersChanged += (bestOffer) => { Console.WriteLine($"Best offer changed, best bid: {bestOffer.BestBid.Price}, best ask: {bestOffer.BestAsk.Price}"); }; book.OnBestOffersChanged += (bestOffer) => { Console.WriteLine($"Best offer changed, best bid: {bestOffer.BestBid.Price}, best ask: {bestOffer.BestAsk.Price}"); };
``` ```
### Order book factory
Each exchange implementation also provides an order book factory for creating ISymbolOrderBook instances. The naming convention for the factory is `[Exchange]OrderBookFactory`, for example `BinanceOrderBookFactory`. This type will be automatically added when using DI and can be used to facilitate easier testing.
*Creating an order book using the order book factory*
```csharp
var factory = services.GetRequiredService<IKucoinOrderBookFactory>();
var book = factory.CreateSpot("ETH-USDT");
var startResult = await book.StartAsync();
```
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
title: Rate limiting title: Rate limiting
nav_order: 7 nav_order: 9
--- ---
## Rate limiting ## Rate limiting
+22 -9
View File
@@ -3,7 +3,7 @@ title: Home
nav_order: 1 nav_order: 1
--- ---
[![.NET](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) ![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg) ![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg) [![.NET](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net)
The CryptoExchange.Net library is a base package for exchange API implementations. It offers base classes for creating clients for exchange API's. Basing exchange implementation on the common CryptoExchange.Net library allows for ease of implementation for new exchanges, as only the endpoints and models have to implemented, but not all systems around requests and connections, and it makes it easier for users to implement a new library in their code base as all base principles and configuration are the same for different exchanges. The CryptoExchange.Net library is a base package for exchange API implementations. It offers base classes for creating clients for exchange API's. Basing exchange implementation on the common CryptoExchange.Net library allows for ease of implementation for new exchanges, as only the endpoints and models have to implemented, but not all systems around requests and connections, and it makes it easier for users to implement a new library in their code base as all base principles and configuration are the same for different exchanges.
@@ -14,13 +14,14 @@ These will always be on the latest CryptoExchange.Net version and the latest ver
|-|-|-| |-|-|-|
|<a href="https://github.com/JKorf/Binance.Net"><img src="https://github.com/JKorf/Binance.Net/blob/master/Binance.Net/Icon/icon.png?raw=true"></a>|Binance|https://jkorf.github.io/Binance.Net/| |<a href="https://github.com/JKorf/Binance.Net"><img src="https://github.com/JKorf/Binance.Net/blob/master/Binance.Net/Icon/icon.png?raw=true"></a>|Binance|https://jkorf.github.io/Binance.Net/|
|<a href="https://github.com/JKorf/Bitfinex.Net"><img src="https://github.com/JKorf/Bitfinex.Net/blob/master/Bitfinex.Net/Icon/icon.png?raw=true"></a>|Bitfinex|https://jkorf.github.io/Bitfinex.Net/| |<a href="https://github.com/JKorf/Bitfinex.Net"><img src="https://github.com/JKorf/Bitfinex.Net/blob/master/Bitfinex.Net/Icon/icon.png?raw=true"></a>|Bitfinex|https://jkorf.github.io/Bitfinex.Net/|
|<a href="https://github.com/JKorf/Bitget.Net"><img src="https://github.com/JKorf/Bitget.Net/blob/master/Bitget.Net/Icon/icon.png?raw=true"></a>|Bitget|https://jkorf.github.io/Bitget.Net/|
|<a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Bittrex.Net/Icon/icon.png?raw=true"></a>|Bittrex|https://jkorf.github.io/Bittrex.Net/| |<a href="https://github.com/JKorf/Bittrex.Net"><img src="https://github.com/JKorf/Bittrex.Net/blob/master/Bittrex.Net/Icon/icon.png?raw=true"></a>|Bittrex|https://jkorf.github.io/Bittrex.Net/|
|<a href="https://github.com/JKorf/Bybit.Net"><img src="https://github.com/JKorf/Bybit.Net/blob/main/ByBit.Net/Icon/icon.png?raw=true"></a>|Bybit|https://jkorf.github.io/Bybit.Net/| |<a href="https://github.com/JKorf/Bybit.Net"><img src="https://github.com/JKorf/Bybit.Net/blob/main/ByBit.Net/Icon/icon.png?raw=true"></a>|Bybit|https://jkorf.github.io/Bybit.Net/|
|<a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/CoinEx.Net/Icon/icon.png?raw=true"></a>|CoinEx|https://jkorf.github.io/CoinEx.Net/| |<a href="https://github.com/JKorf/CoinEx.Net"><img src="https://github.com/JKorf/CoinEx.Net/blob/master/CoinEx.Net/Icon/icon.png?raw=true"></a>|CoinEx|https://jkorf.github.io/CoinEx.Net/|
|<a href="https://github.com/JKorf/FTX.Net"><img src="https://github.com/JKorf/FTX.Net/blob/main/FTX.Net/Icon/icon.png?raw=true"></a>|FTX|https://jkorf.github.io/FTX.Net/|
|<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/| |<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/|
|<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/| |<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/|
|<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/| |<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/|
|<a href="https://github.com/JKorf/OKX.Net"><img src="https://raw.githubusercontent.com/JKorf/OKX.Net/358d31f58d8ee51fc234bff1940878a8d0ce5676/Okex.Net/Icon/icon.png"></a>|OKX|https://jkorf.github.io/OKX.Net/|
**Implementations by third parties** **Implementations by third parties**
These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version. These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version.
@@ -32,7 +33,6 @@ These might not be compatible with other libraries, make sure to check the Crypt
|<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex| |<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex|
|<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC| |<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC|
|<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin| |<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin|
|<a href="https://github.com/burakoner/OKEx.Net"><img src="https://github.com/burakoner/OKEx.Net/blob/master/Okex.Net/Icon/icon.png?raw=true"></a>|OKEx|
|<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz| |<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz|
|<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk| |<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk|
|<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex| |<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex|
@@ -42,11 +42,24 @@ These might not be compatible with other libraries, make sure to check the Crypt
## Discord ## Discord
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries. A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
## Donate / Sponsor ## Support the project
I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me. I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS ### Referral link
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2 Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs [Binance](https://accounts.binance.com/en/register?ref=10153680)
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
[Bybit](https://partner.bybit.com/b/jkorf)
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf) ### Donate
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
**Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
**Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
### Sponsor
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).