mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-07-16 22:46:13 +00:00
Squashed commit of the following:
commit 9450d447b9822470504e3031e57a65146c838e0e Author: Jkorf <jankorf91@gmail.com> Date: Fri Feb 18 11:05:46 2022 +0100 Updated version commit bc0b55f3372f32bf7dd6947f4ea4f02f1bfeaa05 Author: Jkorf <jankorf91@gmail.com> Date: Fri Feb 18 10:09:26 2022 +0100 Added clientOrderId parameter to common clients commit 31111006c728d4d1b513c32838ca5b92e33a4c4a Author: Jkorf <jankorf91@gmail.com> Date: Thu Feb 17 16:32:53 2022 +0100 Update SpotClient.razor commit e7400ce334175961426daffd6827e08349e518b6 Author: Jkorf <jankorf91@gmail.com> Date: Thu Feb 17 16:10:49 2022 +0100 Made some names more generic commit 9bdef400daaed68d48f4be2c0a3311498bac5b1c Author: Jkorf <jankorf91@gmail.com> Date: Tue Feb 15 11:38:41 2022 +0100 Updated vesrion commit 3b80a945eef9c42de8b19850b2e0fe45f2d6caa0 Author: Jkorf <jankorf91@gmail.com> Date: Tue Feb 15 11:34:50 2022 +0100 docs commit 0268e211e90956016652280c6d2b9b7ec4c4e701 Author: Jkorf <jankorf91@gmail.com> Date: Tue Feb 15 09:56:45 2022 +0100 Immediate initial reconnect attempt when connection is lost commit 6eb43c5218fcaab2e51538a93776d538f9b9e7fc Author: Jkorf <jankorf91@gmail.com> Date: Fri Feb 11 13:59:05 2022 +0100 Re-added recalculation interval commit 1df63ab60c5e0f63f64d16a07ae452dd6bd92ee3 Author: Jkorf <jankorf91@gmail.com> Date: Wed Feb 9 14:32:00 2022 +0100 Updated version commit 9461b57daa9ae4d74702b38de15cf2ee8c461263 Author: Jkorf <jankorf91@gmail.com> Date: Wed Feb 9 13:37:12 2022 +0100 Fix for time offset calculation not updating when offset is < 500ms commit 105547d6b16d99258adb96c150bef7ffbc82b487 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Feb 5 21:05:10 2022 +0100 Updated version commit 379ded6832d25ada47519f979c43c05daaf4d17c Author: Jan Korf <jankorf91@gmail.com> Date: Sat Feb 5 20:29:57 2022 +0100 Fixed tests commit b18204a52d8c26650059fc88631ad9eccc505f15 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Feb 5 20:28:08 2022 +0100 Added CancellationToken support on Common client interface and SymbolOrderBook, improved SymbolOrderBook start/stop robustness commit baa23c2eccb6f84c875be3c60e77c395e8b7cd90 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Feb 5 14:56:32 2022 +0100 Added GetSubscriptionByRequest method on socket connection commit 7aad9482a540865c4f83bea7aaf763979e02cdba Author: Jkorf <jankorf91@gmail.com> Date: Wed Feb 2 10:57:06 2022 +0100 Updated version commit 6e4d9d225eb4076a3c2c6586c5a4cec391e04d00 Author: Jkorf <jankorf91@gmail.com> Date: Wed Feb 2 09:42:22 2022 +0100 Fixed exception when deserializing non-nullable datetime value '0' in .net framework commit fd1a2bbda95314f4b03d1d0e0078171238429c7c Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 25 13:19:10 2022 +0100 Updated version commit 2ece04dd58f7524e4d50447fe1813d1c3ef7e5d4 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 25 13:17:25 2022 +0100 Refactored use of AutoResetEvent to AsyncResetEvent in SymbolOrderBook commit 893d0c723d55c026ab854d0945a03186828d59b3 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 25 13:01:21 2022 +0100 Fixed DateTime converter for nanosecond times in string format commit 2c43ee7554af43adee40082b4500bc1e9c041d36 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 24 15:56:24 2022 +0100 Updated version version; fixed dependencies commit 100a34d1a0372940dd6f02599c46306fed263c6c Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 24 14:37:15 2022 +0100 Updated version commit bb1071472f4170c2f250e8c3775e888b02b7a1ed Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 24 14:31:57 2022 +0100 Re-added Common prefix for common enums to avoid conflicts with library namespaces commit 37b1d18104851797e3bc617c976ad2962f183b90 Author: Jkorf <jankorf91@gmail.com> Date: Fri Jan 21 15:25:33 2022 +0100 Updated version commit 325389cdf81e51ef829bb2c5edd45cd4adc00d09 Author: Jan Korf <jankorf91@gmail.com> Date: Thu Jan 20 21:08:51 2022 +0100 Added FTX to console example commit 3e23882572e42b54e30c0727f4002a8c3d620b88 Author: Jkorf <jankorf91@gmail.com> Date: Thu Jan 20 16:21:42 2022 +0100 Replaced Debug.WriteLine with Trace.WriteLine commit 3cf5480cad23db99711486afbdb71e242f65de76 Author: Jan Korf <jankorf91@gmail.com> Date: Wed Jan 19 22:07:22 2022 +0100 Example commit fe31cf156d76c41159ff4ffee02c720929a2aaa4 Author: Jkorf <jankorf91@gmail.com> Date: Wed Jan 19 16:35:08 2022 +0100 Examples commit 7427914cb76cc44dda2c095e90bf89a04cf802d0 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 16:46:43 2022 +0100 Update index.md commit 1bc62258140d4f11c3348ea6f32fc81ff67e0186 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 16:45:10 2022 +0100 docs commit 259fe6bfd12026161aa6bdb167e0a9da3e5eca6e Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 14:25:20 2022 +0100 Update index.md commit 5f9c075ac7fc8ae1d35b71ea8db99168c2945511 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 14:22:33 2022 +0100 Update index.md commit a26514016a0cebb86117be25987e425e47bfb2f9 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 14:13:35 2022 +0100 Update index.md commit 01a97412bffcbded0ade5406a0e482236fa6f3f4 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 14:12:02 2022 +0100 docs commit 24b503ca8cdeb7d1ab98467a38b6fc4905d53246 Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 13:42:12 2022 +0100 docs commit 008b15b055bf6c4793f0ca26bca8a302f0b1614a Author: Jkorf <jankorf91@gmail.com> Date: Tue Jan 18 13:32:55 2022 +0100 docs commit 66fce6cb849ae86b0b71bdb625c2b4f905a0ba33 Author: Jan Korf <jankorf91@gmail.com> Date: Mon Jan 17 21:31:53 2022 +0100 docs commit 0f65701f902bfb290d4589d05debc2de4b6d5705 Author: Jan Korf <jankorf91@gmail.com> Date: Mon Jan 17 21:25:33 2022 +0100 docs commit f7a405a2e6e518ff1d9bef0a43ac0e0759aea1d2 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 16:32:50 2022 +0100 docs commit 55284c0549a38ae88cc7fe0c9f85fe8326bb1f3d Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 15:51:03 2022 +0100 docs commit 5bfbcca25bf84ab429007555c9b87331d867172f Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 14:04:08 2022 +0100 docs commit cdbc0ba215cb978b0bc3e3d2fe23b874b3c07256 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 13:58:51 2022 +0100 docs commit e33e7c6775b9a355bdaf38a407bd3d4c09809ccb Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 13:47:58 2022 +0100 docs commit b65669659d189066d0ef6e19ff9c02fb27cd18f1 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 13:44:45 2022 +0100 docs commit e51b8632424965e25cee5f70386aed9b20601255 Merge: dbfe34f 088f35d Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 13:36:51 2022 +0100 Merge branch 'feature/new-cc' of https://github.com/JKorf/CryptoExchange.Net into feature/new-cc commit dbfe34f53449c1d84948500d36fc0af7278befa9 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 17 13:35:46 2022 +0100 Docs commit 088f35d42099a60c8a820c603507b187f4038460 Author: Jan Korf <jankorf91@gmail.com> Date: Mon Jan 17 13:34:40 2022 +0100 Set theme jekyll-theme-cayman commit e77add4d1c84ccf1a7e8b55859883be36d72bdc3 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Jan 15 15:26:38 2022 +0100 Updated version commit a37a2d6e31e3bbf13ed745761bc75c17638bddc2 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Jan 15 15:23:52 2022 +0100 Added CallResult tests, fixed response time not set commit 8f6e853e13756260b78ff3b718ea5e6d200bab3e Author: Jkorf <jankorf91@gmail.com> Date: Fri Jan 14 16:47:49 2022 +0100 Added Request info and ResponseTime to WebCallResult, refactored CallResult ctors commit c6bf0d67a45ae85f3b022b1257f4d9eeb322fa8e Author: Jkorf <jankorf91@gmail.com> Date: Fri Jan 7 16:30:02 2022 +0100 Fix typo commit 996f3c2ced8caa8022f3e8b4e3c166b345a98842 Author: Jkorf <jankorf91@gmail.com> Date: Fri Jan 7 16:23:42 2022 +0100 Some options logging commit fb9e9f9aa65b0fdd5866387316e9277f218482f1 Author: Jkorf <jankorf91@gmail.com> Date: Fri Jan 7 15:10:27 2022 +0100 Updated version commit 52ebacaa212b1c663cdf7433876776f350692f3e Author: Jkorf <jankorf91@gmail.com> Date: Fri Jan 7 15:05:51 2022 +0100 Fixed symbol order book tostring not locking thread, Potential fix for request timeout showing unclear message commit 6b4585993450daba95b84292fdc0d73f0f9dc7b7 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 3 14:08:35 2022 +0100 Updated example commit ebe332b724fbe4f7c9e2b2bf4864049e7dfa31d6 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 3 12:05:47 2022 +0100 Updated version commit 8c24b46fb32408afacea86c9504f4a463fae3c75 Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 3 11:33:07 2022 +0100 Fixed typo Comon -> Common commit 7a195f662c6c339d7d69e88025803f497f1ae30d Author: Jkorf <jankorf91@gmail.com> Date: Mon Jan 3 09:37:50 2022 +0100 Updated example, removed global.json commit 120132c45b9b8cc7703d0b9a9bbdba1f54e92f9b Author: Jan Korf <jankorf91@gmail.com> Date: Sat Jan 1 20:30:35 2022 +0100 Reverted conditional refs commit b3b4ed3f3fd0fd0fa536e1fa245c05fdb65633e0 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Jan 1 19:45:59 2022 +0100 Updated version commit f4b4c93e6473961875f114b47d3434714f57c8b9 Author: Jan Korf <jankorf91@gmail.com> Date: Sat Jan 1 19:40:50 2022 +0100 Added new shared interface implementation commit f8c3b37cdf3baa42715cf5b31e5884fd9f077db4 Author: Jan Korf <jankorf91@gmail.com> Date: Tue Dec 28 14:14:15 2021 +0100 wip example commit 0117737dfacb8f37f087ab8d59ae806ab66b1e55 Author: Jan Korf <jankorf91@gmail.com> Date: Tue Dec 28 14:13:14 2021 +0100 Added conditional refs for Microsoft.Extensions, added DependencyInjection.Abstractions to support extension method on IServiceCollection commit 02c1f874e17a9fae1579ed2ce5e8a6db99377738 Author: Jan Korf <jankorf91@gmail.com> Date: Mon Dec 27 15:32:07 2021 +0100 Updated version commit b212842ec8048be584ae034c9cf2c28a97ecfa3e Author: Jan Korf <jankorf91@gmail.com> Date: Mon Dec 27 15:27:14 2021 +0100 Added ExchangeName to IExchangeClient interface commit c96e75d6c3ef0e28a492755b0c2bf2cc2531f4e1 Author: Jkorf <jankorf91@gmail.com> Date: Tue Dec 21 16:22:51 2021 +0100 Updated version commit c62fbda3d74c00c11e030f250a91494ab4b538d9 Author: Jkorf <jankorf91@gmail.com> Date: Fri Dec 17 14:17:30 2021 +0100 Added ApiClients list for managing api credentials, requests made and dispose commit 04b43257a549666d793674931640810c8f276c1e Author: Jkorf <jankorf91@gmail.com> Date: Thu Dec 16 16:17:26 2021 +0100 Update .gitignore commit 8ba0ded16d12a7fadffe87d7819a7ddd9df457bc Author: Jkorf <jankorf91@gmail.com> Date: Mon Dec 13 12:57:31 2021 +0100 Fixed api credentials getting disposed, fixed DateTimeConverter losing precision commit 5c665ad54ca40e473b236ddacf54aa9da563320e Author: Jkorf <jankorf91@gmail.com> Date: Fri Dec 10 16:35:42 2021 +0100 Refactoring and comments commit b7cd6a866acf4d91b48d4e50216f6627a299c3a7 Author: Jan Korf <jankorf91@gmail.com> Date: Wed Dec 8 21:49:25 2021 +0100 Auth work commit c2105fe690c6b4a468d3d45e847a2e32172c702a Author: Jkorf <jankorf91@gmail.com> Date: Wed Dec 8 16:20:44 2021 +0100 Wip, support for time syncing, refactoring authentication commit 8b479547ab7330a143502eccb51f0f38e88fe4b9 Author: Jkorf <jankorf91@gmail.com> Date: Tue Dec 7 15:47:55 2021 +0100 Fixed release name commit 2ab032b8718d6dcfbe0904eb5d8ad2cb6c4d2889 Author: Jkorf <jankorf91@gmail.com> Date: Tue Dec 7 15:47:14 2021 +0100 Updated version commit 48baaeb2d8579e4844aaeb3aac30017a71dcb460 Author: Jkorf <jankorf91@gmail.com> Date: Mon Dec 6 16:18:18 2021 +0100 Added periodic identifier commit 60ec18919a080f004bfa1f35dc604d372fc837d9 Author: Jan Korf <jankorf91@gmail.com> Date: Sun Dec 5 17:26:55 2021 +0100 Added quotes to log commit 0818c6277b10f0b334de3145318ac1f6fb1596a8 Author: Jkorf <jankorf91@gmail.com> Date: Fri Dec 3 16:23:05 2021 +0100 Small changes commit 6d0120d564183984d77574921b401127934e43c7 Author: Jkorf <jankorf91@gmail.com> Date: Wed Dec 1 16:26:34 2021 +0100 Comments, fix test commit 3c3b5639f59e07fb5c70d120c15a247d32dfab98 Author: Jkorf <jankorf91@gmail.com> Date: Wed Dec 1 13:31:54 2021 +0100 Refactor clients/options commit 49de7e89ccf6e16dee3ffb10ca77f2f0e2720ac2 Author: Jkorf <jankorf91@gmail.com> Date: Tue Nov 30 10:31:45 2021 +0100 Disposable changes, fixed tests commit 69a6fabb790770b4302e8eb26f9e4acd62cc868d Author: Jkorf <jankorf91@gmail.com> Date: Mon Nov 29 16:43:27 2021 +0100 Restruct commit 9a266e44ced9d9f887fe9e664c1ca393ca008008 Author: Jkorf <jankorf91@gmail.com> Date: Fri Nov 26 09:32:26 2021 +0100 Added enum converter commit 78f81393a441ca34d067a20973e3410761cbf77a Author: Jkorf <jankorf91@gmail.com> Date: Thu Nov 25 10:25:56 2021 +0100 Removed old timestamp converters commit 9ebe5de825ed81a4fe77563d602882f7e9847352 Author: Jan Korf <jankorf91@gmail.com> Date: Wed Nov 24 19:32:37 2021 +0100 Added AppendPath method commit 8b619e82f2953c88e15c1a52e3a09b8de495dfed Author: Jkorf <jankorf91@gmail.com> Date: Wed Nov 24 16:39:14 2021 +0100 Added DateTimeConverter as replacement for individual converters, fix for not closing socket when auth fails commit 7ac7a11dfe87f1ad9b06eaf1327e334f255e0477 Author: Jkorf <jankorf91@gmail.com> Date: Wed Nov 17 10:23:01 2021 +0100 Resolved some code issues commit 3784b0c62b2e0ddba3018fbf18340ee20c32f879 Author: Jkorf <jankorf91@gmail.com> Date: Mon Nov 15 16:36:30 2021 +0100 Ratelimiter rework commit cb1826da7acf730e32bbad43458662ca4e25f35a Author: Jkorf <jankorf91@gmail.com> Date: Fri Nov 12 09:40:42 2021 +0100 Documentation commit f7445543f261d517bfafa4657eba0e4f7b013da7 Author: Jkorf <jankorf91@gmail.com> Date: Wed Nov 10 16:44:46 2021 +0100 Exposed order book id commit 6c3462403f25382365ff8633f62bf8ca3194d343 Author: Jkorf <jankorf91@gmail.com> Date: Wed Nov 10 13:18:52 2021 +0100 Fixed tests commit f83127590ac0b2fd0a9258c21458a05d714a1d14 Author: Jkorf <jankorf91@gmail.com> Date: Wed Nov 3 08:27:03 2021 +0100 wip commit 23bbf0ef8869591e55d9e6822360d9edd9ef6c92 Author: Jkorf <jankorf91@gmail.com> Date: Wed Oct 27 12:57:23 2021 +0200 Added cancellation token support for socket subscriptions commit b7f1619aec8c09b93777bd6319c3adbc8216927b Merge: 6ce6a46 f6af235 Author: Jkorf <jankorf91@gmail.com> Date: Tue Oct 26 15:39:52 2021 +0200 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net commit 6ce6a46ca347468c23e21f561de41ec3fce51e3f Author: Jkorf <jankorf91@gmail.com> Date: Tue Oct 26 15:39:50 2021 +0200 Some renames
This commit is contained in:
parent
8207a8f32a
commit
c22b54c898
@ -11,26 +11,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture()]
|
||||
public class BaseClientTests
|
||||
{
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", "")]
|
||||
[TestCase("test", null)]
|
||||
[TestCase("test", "")]
|
||||
[TestCase(null, "test")]
|
||||
[TestCase("", "test")]
|
||||
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) }));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingLogOutput_Should_RedirectLogOutput()
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new RestClientOptions("")
|
||||
var client = new TestBaseClient(new BaseRestClientOptions()
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
@ -65,16 +51,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(null, LogLevel.Error, true)]
|
||||
[TestCase(null, LogLevel.Warning, true)]
|
||||
[TestCase(null, LogLevel.Information, true)]
|
||||
[TestCase(null, LogLevel.Debug, true)]
|
||||
[TestCase(null, LogLevel.Debug, false)]
|
||||
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new RestClientOptions("")
|
||||
var options = new BaseRestClientOptions()
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger },
|
||||
LogLevel = verbosity
|
||||
});
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
};
|
||||
if (verbosity != null)
|
||||
options.LogLevel = verbosity.Value;
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// act
|
||||
client.Log(testVerbosity, "Test");
|
||||
@ -110,17 +98,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.IsTrue(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void FillingPathParameters_Should_ResultInValidUrl()
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1/", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api/", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
|
||||
[TestCase("https://api.test.com/", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
|
||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.FillParameters("http://test.api/{}/path/{}", "1", "test");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result == "http://test.api/1/path/test");
|
||||
var result = baseUrl.AppendPath(path);
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
176
CryptoExchange.Net.UnitTests/CallResultTests.cs
Normal file
176
CryptoExchange.Net.UnitTests/CallResultTests.cs
Normal file
@ -0,0 +1,176 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
internal class CallResultTests
|
||||
{
|
||||
[Test]
|
||||
public void TestBasicErrorCallResult()
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicSuccessCallResult()
|
||||
{
|
||||
var result = new CallResult(null);
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultError()
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsNull(result.Data);
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccess()
|
||||
{
|
||||
var result = new CallResult<object>(new object());
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsNotNull(result.Data);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccessAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult.Data is TestObject2);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAsError()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultErrorAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
"{}",
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsSuccess()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
"{}",
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObjectResult
|
||||
{
|
||||
public TestObject2 InnerData;
|
||||
|
||||
public TestObjectResult()
|
||||
{
|
||||
InnerData = new TestObject2();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObject2
|
||||
{
|
||||
}
|
||||
}
|
174
CryptoExchange.Net.UnitTests/ConverterTests.cs
Normal file
174
CryptoExchange.Net.UnitTests/ConverterTests.cs
Normal file
@ -0,0 +1,174 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class ConverterTests
|
||||
{
|
||||
[TestCase("2021-05-12")]
|
||||
[TestCase("20210512")]
|
||||
[TestCase("210512")]
|
||||
[TestCase("1620777600.000")]
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||
Assert.AreEqual(output.Time, expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600.000)]
|
||||
[TestCase(1620777600000d)]
|
||||
public void TestDateTimeConverterDouble(double input)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000000)]
|
||||
[TestCase(1620777600000000000)]
|
||||
[TestCase(0, true)]
|
||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.AreEqual(output.Time, expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600.000)]
|
||||
public void TestDateTimeConverterFromSeconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToSeconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000.000)]
|
||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMilliseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000)]
|
||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMicroseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000000)]
|
||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToNanoseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000000000);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void TestDateTimeConverterNull()
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
|
||||
Assert.AreEqual(output.Time, null);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.AreEqual(output, expected);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.AreEqual(output, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", null)]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
}
|
||||
|
||||
public class TimeObject
|
||||
{
|
||||
[JsonConverter(typeof(DateTimeConverter))]
|
||||
public DateTime? Time { get; set; }
|
||||
}
|
||||
|
||||
public class EnumObject
|
||||
{
|
||||
public TestEnum? Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public enum TestEnum
|
||||
{
|
||||
[Map("1")]
|
||||
One,
|
||||
[Map("2")]
|
||||
Two,
|
||||
[Map("three", "3")]
|
||||
Three,
|
||||
Four
|
||||
}
|
||||
}
|
@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"></packagereference>
|
||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></packagereference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
|
||||
<packagereference Include="NUnit3TestAdapter" Version="3.17.0"></packagereference>
|
||||
<packagereference Include="NUnit3TestAdapter" Version="4.2.0"></packagereference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
308
CryptoExchange.Net.UnitTests/OptionsTests.cs
Normal file
308
CryptoExchange.Net.UnitTests/OptionsTests.cs
Normal file
@ -0,0 +1,308 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class OptionsTests
|
||||
{
|
||||
[TearDown]
|
||||
public void Init()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", "")]
|
||||
[TestCase("test", null)]
|
||||
[TestCase("test", "")]
|
||||
[TestCase(null, "test")]
|
||||
[TestCase("", "test")]
|
||||
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException),
|
||||
() => new RestApiClientOptions() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApiOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
BaseAddress = "http://test1.com"
|
||||
},
|
||||
Api2Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("789", "101"),
|
||||
BaseAddress = "http://test2.com"
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "http://test1.com");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||
Assert.AreEqual(options.Api2Options.BaseAddress, "http://test2.com");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotOverridenApiOptionsAreStillDefault()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.RateLimitingBehaviour, RateLimitingBehaviour.Wait);
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||
Assert.AreEqual(options.Api2Options.BaseAddress, "https://api2.test.com/");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultBaseOptionsAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.LogLevel, LogLevel.Trace);
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultApiOptionsAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("456", "789")
|
||||
}
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.BaseAddress, "https://api1.test.com/");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "789");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSettingDefaultApiOptionsWithSomeOverriddenAreRespected()
|
||||
{
|
||||
// arrange
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
LogLevel = LogLevel.Trace,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("456", "789")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
|
||||
// act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
}
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "222");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
|
||||
var client = new TestRestClient();
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "111");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "222");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions()
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("111", "222")
|
||||
}
|
||||
};
|
||||
|
||||
var client = new TestRestClient(new TestClientOptions
|
||||
{
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("333", "444")
|
||||
},
|
||||
Api2Options = new RestApiClientOptions()
|
||||
{
|
||||
BaseAddress = "http://test.com"
|
||||
}
|
||||
});
|
||||
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Key.GetString(), "333");
|
||||
Assert.AreEqual(client.Api1.AuthenticationProvider.Credentials.Secret.GetString(), "444");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(client.Api2.AuthenticationProvider.Credentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "http://test.com");
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClientOptions: BaseRestClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </summary>
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||
{
|
||||
Api1Options = new RestApiClientOptions(),
|
||||
Api2Options = new RestApiClientOptions()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
private RestApiClientOptions _api1Options = new RestApiClientOptions("https://api1.test.com/");
|
||||
public RestApiClientOptions Api1Options
|
||||
{
|
||||
get => _api1Options;
|
||||
set => _api1Options.Copy(_api1Options, value);
|
||||
}
|
||||
|
||||
private RestApiClientOptions _api2Options = new RestApiClientOptions("https://api2.test.com/");
|
||||
public RestApiClientOptions Api2Options
|
||||
{
|
||||
get => _api2Options;
|
||||
set => _api2Options.Copy(_api2Options, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
if (Default == null)
|
||||
return;
|
||||
|
||||
Copy(this, Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public new void Copy<T>(T input, T def) where T : TestClientOptions
|
||||
{
|
||||
base.Copy(input, def);
|
||||
|
||||
input.ReceiveWindow = def.ReceiveWindow;
|
||||
|
||||
input.Api1Options = new RestApiClientOptions(def.Api1Options);
|
||||
input.Api2Options = new RestApiClientOptions(def.Api2Options);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,10 +8,11 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@ -50,14 +51,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorCode_Should_ResultInError()
|
||||
public async Task ReceivingErrorCode_Should_ResultInError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@ -65,14 +66,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@ -83,14 +84,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
{
|
||||
// arrange
|
||||
var client = new ParseErrorTestRestClient();
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@ -105,20 +106,23 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter>{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))},
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiter() },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||
},
|
||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||
});
|
||||
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
|
||||
Assert.IsTrue(client.RateLimiters.Count() == 1);
|
||||
Assert.IsTrue(client.RateLimitBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(client.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@ -132,9 +136,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var client = new TestRestClient(new TestClientOptions()
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
Api1Options = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
}
|
||||
});
|
||||
|
||||
client.SetParameterPosition(new HttpMethod(method), pos);
|
||||
@ -161,84 +168,199 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests()
|
||||
|
||||
[TestCase(1, 0.1)]
|
||||
[TestCase(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
|
||||
// act
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsFalse(result2.Success);
|
||||
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);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests()
|
||||
[TestCase("/sapi/test1", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/api/test1", false)]
|
||||
[TestCase("sapi/test1", false)]
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
}
|
||||
[TestCase("/sapi/", "/sapi/", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
|
||||
// act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
sw.Stop();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsTrue(result2.Success);
|
||||
Assert.IsTrue(sw.ElapsedMilliseconds > 900, $"Actual: {sw.ElapsedMilliseconds}");
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey()
|
||||
[TestCase(1, 0.1)]
|
||||
[TestCase(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||
LogLevel = LogLevel.Debug,
|
||||
ApiCredentials = new ApiCredentials("TestKey", "TestSecret")
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
// act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetKey("TestKey2", "TestSecret2"); // set to different key
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
client.SetKey("TestKey", "TestSecret"); // set back to original key, should delay
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result3 = client.Request<TestObject>().Result;
|
||||
sw.Stop();
|
||||
[TestCase("/", false)]
|
||||
[TestCase("/sapi/test", true)]
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsTrue(result2.Success);
|
||||
Assert.IsTrue(result3.Success);
|
||||
Assert.IsTrue(sw.ElapsedMilliseconds > 900 && sw.ElapsedMilliseconds < 1900, $"Actual: {sw.ElapsedMilliseconds}");
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("/", false)]
|
||||
[TestCase("/sapi/test", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test1", "/api/test2", true)]
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", true, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, true, false)]
|
||||
[TestCase("/sapi/test", false, 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)
|
||||
{
|
||||
var log = new Log("Test");
|
||||
log.Level = LogLevel.Trace;
|
||||
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(log, endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,16 +17,19 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(new SocketClientOptions("")
|
||||
var client = new TestSocketClient(new TestOptions()
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
SubOptions = new RestApiClientOptions
|
||||
{
|
||||
BaseAddress = "http://test.address.com"
|
||||
},
|
||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||
});
|
||||
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
|
||||
Assert.IsTrue(client.ReconnectInterval.TotalSeconds == 6);
|
||||
Assert.IsTrue(client.SubClient.Options.BaseAddress == "http://test.address.com");
|
||||
Assert.IsTrue(client.ClientOptions.ReconnectInterval.TotalSeconds == 6);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@ -39,7 +42,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
socket.CanConnect = canConnect;
|
||||
|
||||
//act
|
||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, socket));
|
||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, null, socket));
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
@ -49,12 +52,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
@ -77,12 +80,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
@ -105,12 +108,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
bool reconnected = false;
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
sub.ShouldReconnect = true;
|
||||
client.ConnectSocketSub(sub);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
@ -132,10 +135,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
client.ConnectSocketSub(sub);
|
||||
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
||||
|
||||
@ -150,13 +153,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket1 = client.CreateSocket();
|
||||
var socket2 = client.CreateSocket();
|
||||
socket1.CanConnect = true;
|
||||
socket2.CanConnect = true;
|
||||
var sub1 = new SocketConnection(client, socket1);
|
||||
var sub2 = new SocketConnection(client, socket2);
|
||||
var sub1 = new SocketConnection(client, null, socket1);
|
||||
var sub2 = new SocketConnection(client, null, socket2);
|
||||
client.ConnectSocketSub(sub1);
|
||||
client.ConnectSocketSub(sub2);
|
||||
|
||||
@ -172,10 +175,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(new TestOptions() { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(client, null, socket);
|
||||
|
||||
// act
|
||||
var connectResult = client.ConnectSocketSub(sub);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
@ -12,22 +13,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture]
|
||||
public class SymbolOrderBookTests
|
||||
{
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions("Test", true, false);
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
|
||||
public TestableSymbolOrderBook() : base("Test", "BTC/USD", defaultOrderBookOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Dispose() {}
|
||||
|
||||
protected override Task<CallResult<bool>> DoResyncAsync()
|
||||
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
protected override Task<CallResult<UpdateSubscription>> DoStartAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
@ -8,11 +9,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public class TestBaseClient: BaseClient
|
||||
{
|
||||
public TestBaseClient(): base("Test", new RestClientOptions("http://testurl.url"), null)
|
||||
public TestBaseClient(): base("Test", new BaseClientOptions())
|
||||
{
|
||||
}
|
||||
|
||||
public TestBaseClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestBaseClient(BaseRestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@ -23,12 +24,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
return Deserialize<T>(data, false);
|
||||
}
|
||||
|
||||
public string FillParameters(string path, params string[] values)
|
||||
{
|
||||
return FillPathParameter(path, values);
|
||||
return Deserialize<T>(data, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,14 +34,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
|
||||
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)
|
||||
{
|
||||
return base.AddAuthenticationToHeaders(uri, method, parameters, signed, postParameters, arraySerialization);
|
||||
}
|
||||
|
||||
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
return base.AddAuthenticationToParameters(uri, method, parameters, signed, postParameters, arraySerialization);
|
||||
bodyParameters = new SortedDictionary<string, object>();
|
||||
uriParameters = new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public override string Sign(string toSign)
|
||||
|
@ -15,15 +15,19 @@ using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestRestClient: RestClient
|
||||
public class TestRestClient: BaseRestClient
|
||||
{
|
||||
public TestRestClient() : base("Test", new RestClientOptions("http://testurl.url"), null)
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient() : this(new TestClientOptions())
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public TestRestClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestRestClient(TestClientOptions exchangeOptions) : base("Test", exchangeOptions)
|
||||
{
|
||||
Api1 = new TestRestApi1Client(exchangeOptions);
|
||||
Api2 = new TestRestApi2Client(exchangeOptions);
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
@ -32,11 +36,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public void SetKey(string key, string secret)
|
||||
{
|
||||
SetAuthenticationProvider(new UnitTests.TestAuthProvider(new ApiCredentials(key, secret)));
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
{
|
||||
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
|
||||
@ -57,10 +56,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, string, int>((method, uri, id) =>
|
||||
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(new Uri(uri));
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
@ -73,10 +72,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
|
||||
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
@ -99,19 +100,71 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, string, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(new Uri(uri)))
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
return await SendRequestAsync<T>(Api1, new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
public TestRestApi1Client(TestClientOptions options): base(options, options.Api1Options)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override TimeSpan GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
public TestRestApi2Client(TestClientOptions options) : base(options, options.Api2Options)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override TimeSpan GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,12 +173,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
||||
{
|
||||
uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||
bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(providedParameters) : new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
public ParseErrorTestRestClient(RestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
public ParseErrorTestRestClient(TestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
|
||||
protected override Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
@ -9,14 +10,17 @@ using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: SocketClient
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSocketClient() : this(new SocketClientOptions("http://testurl.url"))
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient() : this(new TestOptions())
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(SocketClientOptions exchangeOptions) : base("test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestSocketClient(TestOptions exchangeOptions) : base("test", exchangeOptions)
|
||||
{
|
||||
SubClient = new TestSubSocketClient(exchangeOptions, exchangeOptions.SubOptions);
|
||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
}
|
||||
@ -24,7 +28,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
return (TestSocket)CreateSocket(BaseAddress);
|
||||
return (TestSocket)CreateSocket("123");
|
||||
}
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
@ -43,12 +47,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(JToken message, object request)
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(JToken message, string identifier)
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -63,4 +67,21 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
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,12 +1,18 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2008
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net.UnitTests", "CryptoExchange.Net.UnitTests\CryptoExchange.Net.UnitTests.csproj", "{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\BlazorClient\BlazorClient.csproj", "{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -21,10 +27,22 @@ Global
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||
EndGlobalSection
|
||||
|
@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
|
||||
/// <summary>
|
||||
/// Used for conversion in ArrayConverter
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class JsonConversionAttribute: Attribute
|
||||
{
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks property as optional
|
||||
/// </summary>
|
||||
public class JsonOptionalPropertyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
24
CryptoExchange.Net/Attributes/MapAttribute.cs
Normal file
24
CryptoExchange.Net/Attributes/MapAttribute.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Map a enum entry to string values
|
||||
/// </summary>
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Values mapping to the enum entry
|
||||
/// </summary>
|
||||
public string[] Values { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="maps"></param>
|
||||
public MapAttribute(params string[] maps)
|
||||
{
|
||||
Values = maps;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ using Newtonsoft.Json.Linq;
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials info
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials: IDisposable
|
||||
{
|
||||
@ -67,7 +67,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// Copy the credentials
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ApiCredentials Copy()
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
if (PrivateKey == null)
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||
|
@ -1,6 +1,11 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
@ -14,45 +19,158 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
public ApiCredentials Credentials { get; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
if (credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
Credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add authentication to the parameter list based on the provided credentials
|
||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri the request is for</param>
|
||||
/// <param name="method">The HTTP method of the request</param>
|
||||
/// <param name="parameters">The provided parameters for the request</param>
|
||||
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
|
||||
/// <param name="parameterPosition">Where parameters are placed, in the URI or in the request body</param>
|
||||
/// <param name="arraySerialization">How array parameters are serialized</param>
|
||||
/// <returns>Should return the original parameter list including any authentication parameters needed</returns>
|
||||
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
|
||||
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for the request</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="providedParameters">The request parameters</param>
|
||||
/// <param name="auth">If the requests should be authenticated</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||
/// <param name="headers">The headers that should be send with the request</param>
|
||||
public abstract 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
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA256Bytes(string data)
|
||||
{
|
||||
return parameters;
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add authentication to the header dictionary based on the provided credentials
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri the request is for</param>
|
||||
/// <param name="method">The HTTP method of the request</param>
|
||||
/// <param name="parameters">The provided parameters for the request</param>
|
||||
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
|
||||
/// <param name="parameterPosition">Where post parameters are placed, in the URI or in the request body</param>
|
||||
/// <param name="arraySerialization">How array parameters are serialized</param>
|
||||
/// <returns>Should return a dictionary containing any header key/value pairs needed for authenticating the request</returns>
|
||||
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
|
||||
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(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>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(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>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(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>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA512(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -76,16 +194,46 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to hex
|
||||
/// Convert byte array to hex string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string ByteToString(byte[] buff)
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2"); /* hex format */
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to base64 string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string BytesToBase64String(byte[] buff)
|
||||
{
|
||||
return Convert.ToBase64String(buff);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current timestamp including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
CryptoExchange.Net/Authentication/SignOutputType.cs
Normal file
17
CryptoExchange.Net/Authentication/SignOutputType.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Output string type
|
||||
/// </summary>
|
||||
public enum SignOutputType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hex string
|
||||
/// </summary>
|
||||
Hex,
|
||||
/// <summary>
|
||||
/// Base64 string
|
||||
/// </summary>
|
||||
Base64
|
||||
}
|
||||
}
|
@ -1,463 +0,0 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress { get; }
|
||||
/// <summary>
|
||||
/// The name of the exchange the client is for
|
||||
/// </summary>
|
||||
public string ExchangeName { get; }
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal Log log;
|
||||
/// <summary>
|
||||
/// The api proxy
|
||||
/// </summary>
|
||||
protected ApiProxy? apiProxy;
|
||||
/// <summary>
|
||||
/// The authentication provider
|
||||
/// </summary>
|
||||
protected internal AuthenticationProvider? authProvider;
|
||||
/// <summary>
|
||||
/// Should check objects for missing properties based on the model and the received JSON
|
||||
/// </summary>
|
||||
public bool ShouldCheckObjects { get; set; }
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; private set; }
|
||||
/// <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>
|
||||
/// Last id used
|
||||
/// </summary>
|
||||
public static int LastId => lastId;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="exchangeName">The name of the exchange this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
|
||||
protected BaseClient(string exchangeName, ClientOptions options, AuthenticationProvider? authenticationProvider)
|
||||
{
|
||||
log = new Log(exchangeName);
|
||||
authProvider = authenticationProvider;
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
|
||||
ExchangeName = exchangeName;
|
||||
OutputOriginalData = options.OutputOriginalData;
|
||||
BaseAddress = options.BaseAddress;
|
||||
apiProxy = options.Proxy;
|
||||
|
||||
log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
ShouldCheckObjects = options.ShouldCheckObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the authentication provider, can be used when manually setting the API credentials
|
||||
/// </summary>
|
||||
/// <param name="authenticationProvider"></param>
|
||||
protected void SetAuthenticationProvider(AuthenticationProvider authenticationProvider)
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Setting api credentials");
|
||||
authProvider = authenticationProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and returns 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>(null, new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(null, 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="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</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, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(default, tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, checkObject, 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="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</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, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
if ((checkObject ?? ShouldCheckObjects)&& log.Level <= LogLevel.Debug)
|
||||
{
|
||||
// This checks the input JToken object against the class it is being serialized into and outputs any missing fields
|
||||
// in either the input or the class
|
||||
try
|
||||
{
|
||||
if (obj is JObject o)
|
||||
{
|
||||
CheckObject(typeof(T), o, requestId);
|
||||
}
|
||||
else if (obj is JArray j)
|
||||
{
|
||||
if (j.HasValues && j[0] is JObject jObject)
|
||||
CheckObject(typeof(T).GetElementType(), jObject, requestId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer), null);
|
||||
}
|
||||
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>(default, 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>(default, 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>(default, 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)
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
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 || log.Level <= LogLevel.Debug)
|
||||
{
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}");
|
||||
var result = Deserialize<T>(data, null, serializer, requestId);
|
||||
if(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);
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
string data;
|
||||
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 Debug 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>(default, new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek) {
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void CheckObject(Type type, JObject obj, int? requestId = null)
|
||||
{
|
||||
if (type == null)
|
||||
return;
|
||||
|
||||
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
|
||||
// If type has a custom JsonConverter we assume this will handle property mapping
|
||||
return;
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
return;
|
||||
|
||||
if (!obj.HasValues && type != typeof(object))
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var isDif = false;
|
||||
var properties = new List<string>();
|
||||
var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
|
||||
if (ignore != null)
|
||||
continue;
|
||||
|
||||
var propertyName = ((JsonPropertyAttribute?) attr)?.PropertyName;
|
||||
properties.Add(propertyName ?? prop.Name);
|
||||
}
|
||||
foreach (var token in obj)
|
||||
{
|
||||
var d = properties.FirstOrDefault(p => p == token.Key);
|
||||
if (d == null)
|
||||
{
|
||||
d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase));
|
||||
if (d == null)
|
||||
{
|
||||
if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
|
||||
isDif = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
properties.Remove(d);
|
||||
|
||||
var propType = GetProperty(d, props)?.PropertyType;
|
||||
if (propType == null || token.Value == null)
|
||||
continue;
|
||||
if (!IsSimple(propType) && propType != typeof(DateTime))
|
||||
{
|
||||
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
|
||||
CheckObject(propType.GetElementType()!, (JObject)token.Value[0]!, requestId);
|
||||
else if (token.Value is JObject o)
|
||||
CheckObject(propType, o, requestId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var propInfo = props.First(p => p.Name == prop ||
|
||||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
|
||||
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
|
||||
if (optional != null)
|
||||
continue;
|
||||
|
||||
isDif = true;
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
|
||||
}
|
||||
|
||||
if (isDif)
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj);
|
||||
}
|
||||
|
||||
private static PropertyInfo? GetProperty(string name, IEnumerable<PropertyInfo> props)
|
||||
{
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
if (attr == null)
|
||||
{
|
||||
if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase))
|
||||
return prop;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (((JsonPropertyAttribute)attr).PropertyName == name)
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
/// <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 int NextId()
|
||||
{
|
||||
lock (idLock)
|
||||
{
|
||||
lastId += 1;
|
||||
return lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
protected static string FillPathParameter(string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
authProvider?.Credentials?.Dispose();
|
||||
log.Write(LogLevel.Debug, "Disposing exchange client");
|
||||
}
|
||||
}
|
||||
}
|
78
CryptoExchange.Net/Clients/BaseApiClient.cs
Normal file
78
CryptoExchange.Net/Clients/BaseApiClient.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient: IDisposable
|
||||
{
|
||||
private ApiCredentials? _apiCredentials;
|
||||
private AuthenticationProvider? _authenticationProvider;
|
||||
private bool _created;
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_created && _apiCredentials != null)
|
||||
{
|
||||
_authenticationProvider = CreateAuthenticationProvider(_apiCredentials);
|
||||
_created = true;
|
||||
}
|
||||
|
||||
return _authenticationProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The base address for this API client
|
||||
/// </summary>
|
||||
internal protected string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api client options
|
||||
/// </summary>
|
||||
internal ApiClientOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="apiOptions">Api client options</param>
|
||||
protected BaseApiClient(BaseClientOptions options, ApiClientOptions apiOptions)
|
||||
{
|
||||
Options = apiOptions;
|
||||
_apiCredentials = apiOptions.ApiCredentials?.Copy() ?? options.ApiCredentials?.Copy();
|
||||
BaseAddress = apiOptions.BaseAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials(ApiCredentials credentials)
|
||||
{
|
||||
_apiCredentials = credentials;
|
||||
_created = false;
|
||||
_authenticationProvider = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
AuthenticationProvider?.Credentials?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
286
CryptoExchange.Net/Clients/BaseClient.cs
Normal file
286
CryptoExchange.Net/Clients/BaseClient.cs
Normal file
@ -0,0 +1,286 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
internal string Name { get; }
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal Log log;
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
protected static int lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
protected static object idLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public BaseClientOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseClient(string name, BaseClientOptions options)
|
||||
{
|
||||
log = new Log(name);
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
|
||||
ClientOptions = options;
|
||||
|
||||
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>
|
||||
/// Register an API client
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||
{
|
||||
log.Write(LogLevel.Trace, $" {apiClient.GetType().Name} configuration: {apiClient.Options}");
|
||||
ApiClients.Add(apiClient);
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data));
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>( tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= defaultSerializer;
|
||||
|
||||
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.Debug)
|
||||
{
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {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);
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
string data;
|
||||
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 Debug 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)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug 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)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek) {
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug 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>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Disposing client");
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -5,15 +5,11 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@ -24,7 +20,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Base rest client
|
||||
/// </summary>
|
||||
public abstract class RestClient : BaseClient, IRestClient
|
||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating requests. Used for unit testing
|
||||
@ -62,168 +58,102 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for requests. This setting is ignored when injecting a HttpClient in the options, requests timeouts should be set on the client then.
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; }
|
||||
/// <summary>
|
||||
/// What should happen when running into a rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitBehaviour { get; }
|
||||
/// <summary>
|
||||
/// List of rate limiters
|
||||
/// </summary>
|
||||
public IEnumerable<IRateLimiter> RateLimiters { get; private set; }
|
||||
/// <summary>
|
||||
/// Total requests made by this client
|
||||
/// </summary>
|
||||
public int TotalRequestsMade { get; private set; }
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||
|
||||
/// <summary>
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
public new BaseRestClientOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="exchangeName">The name of the exchange this client is for</param>
|
||||
/// <param name="exchangeOptions">The options for this client</param>
|
||||
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
|
||||
protected RestClient(string exchangeName, RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(exchangeName, exchangeOptions, authenticationProvider)
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
|
||||
{
|
||||
if (exchangeOptions == null)
|
||||
throw new ArgumentNullException(nameof(exchangeOptions));
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
RequestTimeout = exchangeOptions.RequestTimeout;
|
||||
RequestFactory.Configure(exchangeOptions.RequestTimeout, exchangeOptions.Proxy, exchangeOptions.HttpClient);
|
||||
RateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in exchangeOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
ClientOptions = options;
|
||||
RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="limiter">The limiter to add</param>
|
||||
public void AddRateLimiter(IRateLimiter limiter)
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials(ApiCredentials credentials)
|
||||
{
|
||||
if (limiter == null)
|
||||
throw new ArgumentNullException(nameof(limiter));
|
||||
|
||||
var rateLimiters = RateLimiters.ToList();
|
||||
rateLimiters.Add(limiter);
|
||||
RateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all rate limiters from this client
|
||||
/// </summary>
|
||||
public void RemoveRateLimiters()
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
public virtual CallResult<long> Ping(CancellationToken ct = default) => PingAsync(ct).Result;
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
public virtual async Task<CallResult<long>> PingAsync(CancellationToken ct = default)
|
||||
{
|
||||
var ping = new Ping();
|
||||
var uri = new Uri(BaseAddress);
|
||||
PingReply reply;
|
||||
|
||||
var ctRegistration = ct.Register(() => ping.SendAsyncCancel());
|
||||
try
|
||||
{
|
||||
reply = await ping.SendPingAsync(uri.Host).ConfigureAwait(false);
|
||||
}
|
||||
catch (PingException e)
|
||||
{
|
||||
if (e.InnerException == null)
|
||||
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + e.Message });
|
||||
|
||||
if (e.InnerException is SocketException exception)
|
||||
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + exception.SocketErrorCode });
|
||||
return new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + e.InnerException.Message });
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctRegistration.Dispose();
|
||||
ping.Dispose();
|
||||
}
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<long>(0, new CancellationRequestedError());
|
||||
|
||||
return reply.Status == IPStatus.Success ? new CallResult<long>(reply.RoundtripTime, null) : new CallResult<long>(0, new CantConnectError { Message = "Ping failed: " + reply.Status });
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <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="checkResult">Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)</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="credits">Credits used for the request</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>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
bool checkResult = true,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int credits = 1,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null) where T : class
|
||||
Dictionary<string, string>? additionalHeaders = null
|
||||
) where T : class
|
||||
{
|
||||
var requestId = NextId();
|
||||
|
||||
if (signed)
|
||||
{
|
||||
var syncTimeResult = await apiClient.SyncTimeAsync().ConfigureAwait(false);
|
||||
if (!syncTimeResult)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
return syncTimeResult.As<T>(default);
|
||||
}
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri);
|
||||
if (signed && authProvider == null)
|
||||
if (signed && apiClient.AuthenticationProvider == null)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
return new WebCallResult<T>(null, null, null, new NoApiCredentialsError());
|
||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||
var request = ConstructRequest(uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||
foreach (var limiter in RateLimiters)
|
||||
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||
foreach (var limiter in apiClient.RateLimiters)
|
||||
{
|
||||
var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour, credits);
|
||||
if (!limitResult.Success)
|
||||
{
|
||||
log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} failed because of rate limit");
|
||||
return new WebCallResult<T>(null, null, null, limitResult.Error);
|
||||
}
|
||||
|
||||
if (limitResult.Data > 0)
|
||||
log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}");
|
||||
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 WebCallResult<T>(limitResult.Error!);
|
||||
}
|
||||
|
||||
string? paramString = "";
|
||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||
paramString = " with request body " + request.Content;
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
|
||||
if (log.Level == LogLevel.Trace)
|
||||
{
|
||||
@ -232,7 +162,8 @@ namespace CryptoExchange.Net
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")}");
|
||||
apiClient.TotalRequestsMade++;
|
||||
log.Write(LogLevel.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||
return await GetResponseAsync<T>(request, deserializer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -247,7 +178,6 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
try
|
||||
{
|
||||
TotalRequestsMade++;
|
||||
var sw = Stopwatch.StartNew();
|
||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
@ -269,16 +199,16 @@ namespace CryptoExchange.Net
|
||||
// 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 WebCallResult<T>.CreateErrorResult(response.StatusCode, response.ResponseHeaders, parseResult.Error!);
|
||||
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 WebCallResult<T>.CreateErrorResult(response.StatusCode, response.ResponseHeaders, error);
|
||||
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, null, deserializer, request.RequestId);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, OutputOriginalData ? data: null, deserializeResult.Data, deserializeResult.Error);
|
||||
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
|
||||
{
|
||||
@ -287,7 +217,7 @@ namespace CryptoExchange.Net
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, OutputOriginalData ? desResult.OriginalData : null, desResult.Data, desResult.Error);
|
||||
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
|
||||
@ -302,7 +232,7 @@ namespace CryptoExchange.Net
|
||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : parseResult.Error!;
|
||||
if(error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
return new WebCallResult<T>(statusCode, headers, default, error);
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
@ -310,21 +240,21 @@ namespace CryptoExchange.Net
|
||||
// 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, default, new WebError(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 (canceledException.CancellationToken == cancellationToken)
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token cancelled by caller
|
||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request cancel requested");
|
||||
return new WebCallResult<T>(null, null, default, new CancellationRequestedError());
|
||||
// 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");
|
||||
return new WebCallResult<T>(null, null, default, new WebError($"[{request.RequestId}] 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -344,6 +274,7 @@ namespace CryptoExchange.Net
|
||||
/// <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>
|
||||
@ -354,6 +285,7 @@ namespace CryptoExchange.Net
|
||||
/// <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,
|
||||
@ -363,45 +295,65 @@ namespace CryptoExchange.Net
|
||||
int requestId,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
if (parameters == null)
|
||||
parameters = new Dictionary<string, object>();
|
||||
|
||||
var uriString = uri.ToString();
|
||||
if (authProvider != null)
|
||||
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, parameterPosition, arraySerialization);
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri && parameters?.Any() == true)
|
||||
uriString += "?" + parameters.CreateParamString(true, arraySerialization);
|
||||
|
||||
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
var request = RequestFactory.Create(method, uriString, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||
{
|
||||
foreach (var parameter in parameters)
|
||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
if (authProvider != null)
|
||||
headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, parameterPosition, arraySerialization);
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
foreach (var header in additionalHeaders)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if(StandardRequestHeaders != null)
|
||||
if (StandardRequestHeaders != null)
|
||||
{
|
||||
foreach (var header in StandardRequestHeaders)
|
||||
// Only add it if it isn't overwritten
|
||||
if(additionalHeaders?.ContainsKey(header.Key) != true)
|
||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
if (parameters?.Any() == true)
|
||||
WriteParamBody(request, parameters, contentType);
|
||||
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
WriteParamBody(request, bodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(requestBodyEmptyContent, contentType);
|
||||
}
|
||||
@ -415,30 +367,18 @@ namespace CryptoExchange.Net
|
||||
/// <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, Dictionary<string, object> parameters, string contentType)
|
||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||
{
|
||||
if (requestBodyFormat == RequestBodyFormat.Json)
|
||||
{
|
||||
// Write the parameters as json in the body
|
||||
var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value));
|
||||
var stringData = JsonConvert.SerializeObject(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (requestBodyFormat == RequestBodyFormat.FormData)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var kvp in parameters.OrderBy(p => p.Key))
|
||||
{
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
foreach (var value in array)
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
}
|
||||
else
|
||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
var stringData = formData.ToString();
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
@ -19,7 +18,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Base for socket client implementations
|
||||
/// </summary>
|
||||
public abstract class SocketClient: BaseClient, ISocketClient
|
||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||
{
|
||||
#region fields
|
||||
/// <summary>
|
||||
@ -30,32 +29,15 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new ConcurrentDictionary<int, SocketConnection>();
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new();
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
/// </summary>
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.ReconnectInterval"/>
|
||||
public TimeSpan ReconnectInterval { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.AutoReconnect"/>
|
||||
public bool AutoReconnect { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
|
||||
public TimeSpan ResponseTimeout { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
|
||||
public TimeSpan SocketNoDataTimeout { get; }
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
/// <summary>
|
||||
/// The max amount of concurrent socket connections
|
||||
/// </summary>
|
||||
public int MaxSocketConnections { get; protected set; } = 9999;
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
|
||||
public int SocketCombineTarget { get; protected set; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxReconnectTries"/>
|
||||
public int? MaxReconnectTries { get; protected set; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxResubscribeTries"/>
|
||||
public int? MaxResubscribeTries { get; protected set; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxConcurrentResubscriptionsPerSocket"/>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; protected set; }
|
||||
protected int MaxSocketConnections { get; set; } = 9999;
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
@ -67,7 +49,7 @@ namespace CryptoExchange.Net
|
||||
/// <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 Dictionary<string, Action<MessageEvent>>();
|
||||
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>
|
||||
@ -97,9 +79,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
get
|
||||
@ -110,27 +90,25 @@ namespace CryptoExchange.Net
|
||||
return sockets.Sum(s => s.Value.Socket.IncomingKbps);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client options
|
||||
/// </summary>
|
||||
public new BaseSocketClientOptions ClientOptions { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="exchangeName">The name of the exchange this client is for</param>
|
||||
/// <param name="exchangeOptions">The options for this client</param>
|
||||
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
|
||||
protected SocketClient(string exchangeName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeName, exchangeOptions, authenticationProvider)
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
protected BaseSocketClient(string name, BaseSocketClientOptions options) : base(name, options)
|
||||
{
|
||||
if (exchangeOptions == null)
|
||||
throw new ArgumentNullException(nameof(exchangeOptions));
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
AutoReconnect = exchangeOptions.AutoReconnect;
|
||||
ReconnectInterval = exchangeOptions.ReconnectInterval;
|
||||
ResponseTimeout = exchangeOptions.SocketResponseTimeout;
|
||||
SocketNoDataTimeout = exchangeOptions.SocketNoDataTimeout;
|
||||
SocketCombineTarget = exchangeOptions.SocketSubscriptionsCombineTarget ?? 1;
|
||||
MaxReconnectTries = exchangeOptions.MaxReconnectTries;
|
||||
MaxResubscribeTries = exchangeOptions.MaxResubscribeTries;
|
||||
MaxConcurrentResubscriptionsPerSocket = exchangeOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||
ClientOptions = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -148,42 +126,54 @@ namespace CryptoExchange.Net
|
||||
/// 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>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler);
|
||||
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>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
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
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
socketConnection = GetSocketConnection(url, authenticated);
|
||||
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
|
||||
if (SocketCombineTarget == 1)
|
||||
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();
|
||||
@ -194,7 +184,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(null, connectResult.Error);
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
if (needsConnecting)
|
||||
log.Write(LogLevel.Debug, $"Socket {socketConnection.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}");
|
||||
@ -208,7 +198,7 @@ namespace CryptoExchange.Net
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} has been paused, can't subscribe at this moment");
|
||||
return new CallResult<UpdateSubscription>(default, new ServerError("Socket is paused"));
|
||||
return new CallResult<UpdateSubscription>( new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
if (request != null)
|
||||
@ -218,7 +208,7 @@ namespace CryptoExchange.Net
|
||||
if (!subResult)
|
||||
{
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(null, subResult.Error);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -228,7 +218,15 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
socketConnection.ShouldReconnect = true;
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription), null);
|
||||
if (ct != default)
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {socketConnection.Socket.Id} Cancellation token set, closing subscription");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}, false);
|
||||
}
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -241,43 +239,51 @@ namespace CryptoExchange.Net
|
||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||
{
|
||||
CallResult<object>? callResult = null;
|
||||
await socketConnection.SendAndWaitAsync(request, ResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||
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);
|
||||
}
|
||||
|
||||
return new CallResult<bool>(callResult?.Success ?? false, callResult == null ? new ServerError("No response on subscription request received"): callResult.Error);
|
||||
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>(object request, bool authenticated)
|
||||
protected virtual Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, object request, bool authenticated)
|
||||
{
|
||||
return QueryAsync<T>(BaseAddress, request, 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>(string url, object request, bool authenticated)
|
||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(SocketApiClient apiClient, string url, object request, bool authenticated)
|
||||
{
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
socketConnection = GetSocketConnection(url, authenticated);
|
||||
if (SocketCombineTarget == 1)
|
||||
socketConnection = GetSocketConnection(apiClient, url, authenticated);
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Can release early when only a single sub per connection
|
||||
semaphoreSlim.Release();
|
||||
@ -286,7 +292,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<T>(default, connectResult.Error);
|
||||
return new CallResult<T>(connectResult.Error!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -299,7 +305,7 @@ namespace CryptoExchange.Net
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} has been paused, can't send query at this moment");
|
||||
return new CallResult<T>(default, new ServerError("Socket is paused"));
|
||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
||||
@ -314,8 +320,8 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
||||
{
|
||||
var dataResult = new CallResult<T>(default, new ServerError("No response on query received"));
|
||||
await socket.SendAndWaitAsync(request, ResponseTimeout, data =>
|
||||
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;
|
||||
@ -336,25 +342,26 @@ namespace CryptoExchange.Net
|
||||
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<bool>(false, connectResult.Error);
|
||||
return new CallResult<bool>(connectResult.Error!);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} authentication failed");
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult<bool>(false, result.Error);
|
||||
return new CallResult<bool>(result.Error);
|
||||
}
|
||||
|
||||
socket.Authenticated = true;
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -389,18 +396,20 @@ namespace CryptoExchange.Net
|
||||
/// 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(JToken message, object request);
|
||||
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(JToken message, string identifier);
|
||||
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>
|
||||
@ -442,18 +451,18 @@ namespace CryptoExchange.Net
|
||||
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));
|
||||
dataHandler(new DataEvent<T>(stringData, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
return;
|
||||
}
|
||||
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData, false);
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||
if (!desResult)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {connection.Socket.Id} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
dataHandler(new DataEvent<T>(desResult.Data, null, ClientOptions.OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp));
|
||||
}
|
||||
|
||||
var subscription = request == null
|
||||
@ -467,7 +476,7 @@ namespace CryptoExchange.Net
|
||||
/// 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(Newtonsoft.Json.Linq.JToken,string)"/>)</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);
|
||||
@ -479,17 +488,19 @@ namespace CryptoExchange.Net
|
||||
/// <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 SocketConnection GetSocketConnection(string address, bool authenticated)
|
||||
protected virtual SocketConnection GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
||||
{
|
||||
var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/')
|
||||
var socketResult = sockets.Where(s => s.Value.Socket.Url.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 < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= SocketCombineTarget)))
|
||||
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.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 result;
|
||||
@ -498,7 +509,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
// Create new socket
|
||||
var socket = CreateSocket(address);
|
||||
var socketConnection = new SocketConnection(this, socket);
|
||||
var socketConnection = new SocketConnection(this, apiClient, socket);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
foreach (var kvp in genericHandlers)
|
||||
{
|
||||
@ -527,11 +538,11 @@ namespace CryptoExchange.Net
|
||||
if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false))
|
||||
{
|
||||
sockets.TryAdd(socketConnection.Socket.Id, socketConnection);
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
socketConnection.Socket.Dispose();
|
||||
return new CallResult<bool>(false, new CantConnectError());
|
||||
return new CallResult<bool>(new CantConnectError());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -544,10 +555,10 @@ namespace CryptoExchange.Net
|
||||
var socket = SocketFactory.CreateWebsocket(log, address);
|
||||
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||
|
||||
if (apiProxy != null)
|
||||
socket.SetProxy(apiProxy);
|
||||
if (ClientOptions.Proxy != null)
|
||||
socket.SetProxy(ClientOptions.Proxy);
|
||||
|
||||
socket.Timeout = SocketNoDataTimeout;
|
||||
socket.Timeout = ClientOptions.SocketNoDataTimeout;
|
||||
socket.DataInterpreterBytes = dataInterpreterBytes;
|
||||
socket.DataInterpreterString = dataInterpreterString;
|
||||
socket.RatelimitPerSecond = RateLimitPerSocketPerSecond;
|
||||
@ -564,9 +575,10 @@ namespace CryptoExchange.Net
|
||||
/// <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(TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
{
|
||||
if (objGetter == null)
|
||||
throw new ArgumentNullException(nameof(objGetter));
|
||||
@ -592,7 +604,7 @@ namespace CryptoExchange.Net
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic");
|
||||
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic {identifier}");
|
||||
|
||||
try
|
||||
{
|
||||
@ -600,7 +612,7 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} Periodic send failed: " + ex);
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} Periodic send {identifier} failed: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
103
CryptoExchange.Net/Clients/RestApiClient.cs
Normal file
103
CryptoExchange.Net/Clients/RestApiClient.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client for interacting with a REST API
|
||||
/// </summary>
|
||||
public abstract class RestApiClient: BaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Get time sync info for an API client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract TimeSyncInfo GetTimeSyncInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Get time offset for an API client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract TimeSpan GetTimeOffset();
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
public int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for this client
|
||||
/// </summary>
|
||||
public new RestApiClientOptions Options => (RestApiClientOptions)base.Options;
|
||||
|
||||
/// <summary>
|
||||
/// List of rate limiters
|
||||
/// </summary>
|
||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
/// <returns>Server time</returns>
|
||||
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
|
||||
|
||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
var result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return result.As(false);
|
||||
}
|
||||
|
||||
if (TotalRequestsMade == 1)
|
||||
{
|
||||
// If this was the first request make another one to calculate the offset since the first one can be slower
|
||||
localTime = DateTime.UtcNow;
|
||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return result.As(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate time offset between local and server
|
||||
var offset = result.Data - localTime;
|
||||
timeSyncParams.UpdateTimeOffset(offset);
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
}
|
||||
}
|
19
CryptoExchange.Net/Clients/SocketApiClient.cs
Normal file
19
CryptoExchange.Net/Clients/SocketApiClient.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base socket API client for interaction with a websocket API
|
||||
/// </summary>
|
||||
public abstract class SocketApiClient : BaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public SocketApiClient(BaseClientOptions options, ApiClientOptions apiOptions): base(options, apiOptions)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
21
CryptoExchange.Net/CommonObjects/Balance.cs
Normal file
21
CryptoExchange.Net/CommonObjects/Balance.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Balance data
|
||||
/// </summary>
|
||||
public class Balance: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset name
|
||||
/// </summary>
|
||||
public string Asset { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Quantity available
|
||||
/// </summary>
|
||||
public decimal? Available { get; set; }
|
||||
/// <summary>
|
||||
/// Total quantity
|
||||
/// </summary>
|
||||
public decimal? Total { get; set; }
|
||||
}
|
||||
}
|
13
CryptoExchange.Net/CommonObjects/BaseComonObject.cs
Normal file
13
CryptoExchange.Net/CommonObjects/BaseComonObject.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for common objects
|
||||
/// </summary>
|
||||
public class BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The source object the data is derived from
|
||||
/// </summary>
|
||||
public object SourceObject { get; set; } = null!;
|
||||
}
|
||||
}
|
77
CryptoExchange.Net/CommonObjects/Enums.cs
Normal file
77
CryptoExchange.Net/CommonObjects/Enums.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order type
|
||||
/// </summary>
|
||||
public enum CommonOrderType
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit type
|
||||
/// </summary>
|
||||
Limit,
|
||||
/// <summary>
|
||||
/// Market type
|
||||
/// </summary>
|
||||
Market,
|
||||
/// <summary>
|
||||
/// Other order type
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order side
|
||||
/// </summary>
|
||||
public enum CommonOrderSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Buy order
|
||||
/// </summary>
|
||||
Buy,
|
||||
/// <summary>
|
||||
/// Sell order
|
||||
/// </summary>
|
||||
Sell
|
||||
}
|
||||
/// <summary>
|
||||
/// Order status
|
||||
/// </summary>
|
||||
public enum CommonOrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// placed and not fully filled order
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// canceled order
|
||||
/// </summary>
|
||||
Canceled,
|
||||
/// <summary>
|
||||
/// filled order
|
||||
/// </summary>
|
||||
Filled
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public enum CommonPositionSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Long position
|
||||
/// </summary>
|
||||
Long,
|
||||
/// <summary>
|
||||
/// Short position
|
||||
/// </summary>
|
||||
Short,
|
||||
/// <summary>
|
||||
/// Both
|
||||
/// </summary>
|
||||
Both
|
||||
}
|
||||
}
|
35
CryptoExchange.Net/CommonObjects/Kline.cs
Normal file
35
CryptoExchange.Net/CommonObjects/Kline.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Kline data
|
||||
/// </summary>
|
||||
public class Kline: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Opening time of the kline
|
||||
/// </summary>
|
||||
public DateTime OpenTime { get; set; }
|
||||
/// <summary>
|
||||
/// Price at the open time
|
||||
/// </summary>
|
||||
public decimal? OpenPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Highest price of the kline
|
||||
/// </summary>
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Lowest price of the kline
|
||||
/// </summary>
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Close price of the kline
|
||||
/// </summary>
|
||||
public decimal? ClosePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the kline
|
||||
/// </summary>
|
||||
public decimal? Volume { get; set; }
|
||||
}
|
||||
}
|
47
CryptoExchange.Net/CommonObjects/Order.cs
Normal file
47
CryptoExchange.Net/CommonObjects/Order.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order data
|
||||
/// </summary>
|
||||
public class Order: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the order
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Symbol of the order
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
public decimal? Price { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// The quantity of the order which has been filled
|
||||
/// </summary>
|
||||
public decimal? QuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Status of the order
|
||||
/// </summary>
|
||||
public CommonOrderStatus Status { get; set; }
|
||||
/// <summary>
|
||||
/// Side of the order
|
||||
/// </summary>
|
||||
public CommonOrderSide Side { get; set; }
|
||||
/// <summary>
|
||||
/// Type of the order
|
||||
/// </summary>
|
||||
public CommonOrderType Type { get; set; }
|
||||
/// <summary>
|
||||
/// Order time
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
21
CryptoExchange.Net/CommonObjects/OrderBook.cs
Normal file
21
CryptoExchange.Net/CommonObjects/OrderBook.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book data
|
||||
/// </summary>
|
||||
public class OrderBook: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// List of bids
|
||||
/// </summary>
|
||||
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
|
||||
/// <summary>
|
||||
/// List of asks
|
||||
/// </summary>
|
||||
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
|
||||
}
|
||||
}
|
17
CryptoExchange.Net/CommonObjects/OrderBookEntry.cs
Normal file
17
CryptoExchange.Net/CommonObjects/OrderBookEntry.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book entry
|
||||
/// </summary>
|
||||
public class OrderBookEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Quantity of the entry
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Price of the entry
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
}
|
17
CryptoExchange.Net/CommonObjects/OrderId.cs
Normal file
17
CryptoExchange.Net/CommonObjects/OrderId.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of an order
|
||||
/// </summary>
|
||||
public class OrderId: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of an order
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
65
CryptoExchange.Net/CommonObjects/Position.cs
Normal file
65
CryptoExchange.Net/CommonObjects/Position.cs
Normal file
@ -0,0 +1,65 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Position data
|
||||
/// </summary>
|
||||
public class Position: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the position
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
/// <summary>
|
||||
/// Symbol of the position
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Leverage
|
||||
/// </summary>
|
||||
public decimal Leverage { get; set; }
|
||||
/// <summary>
|
||||
/// Position quantity
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Entry price
|
||||
/// </summary>
|
||||
public decimal? EntryPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Liquidation price
|
||||
/// </summary>
|
||||
public decimal? LiquidationPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Unrealized profit and loss
|
||||
/// </summary>
|
||||
public decimal? UnrealizedPnl { get; set; }
|
||||
/// <summary>
|
||||
/// Realized profit and loss
|
||||
/// </summary>
|
||||
public decimal? RealizedPnl { get; set; }
|
||||
/// <summary>
|
||||
/// Mark price
|
||||
/// </summary>
|
||||
public decimal? MarkPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Auto adding margin
|
||||
/// </summary>
|
||||
public bool? AutoMargin { get; set; }
|
||||
/// <summary>
|
||||
/// Position margin
|
||||
/// </summary>
|
||||
public decimal? PositionMargin { get; set; }
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public CommonPositionSide? Side { get; set; }
|
||||
/// <summary>
|
||||
/// Is isolated
|
||||
/// </summary>
|
||||
public bool? Isolated { get; set; }
|
||||
/// <summary>
|
||||
/// Maintenance margin
|
||||
/// </summary>
|
||||
public decimal? MaintananceMargin { get; set; }
|
||||
}
|
||||
}
|
33
CryptoExchange.Net/CommonObjects/Symbol.cs
Normal file
33
CryptoExchange.Net/CommonObjects/Symbol.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol data
|
||||
/// </summary>
|
||||
public class Symbol: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the symbol
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Minimal quantity of an order
|
||||
/// </summary>
|
||||
public decimal? MinTradeQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Step with which the quantity should increase
|
||||
/// </summary>
|
||||
public decimal? QuantityStep { get; set; }
|
||||
/// <summary>
|
||||
/// step with which the price should increase
|
||||
/// </summary>
|
||||
public decimal? PriceStep { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of decimals for quantity
|
||||
/// </summary>
|
||||
public int? QuantityDecimals { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of decimal for price
|
||||
/// </summary>
|
||||
public int? PriceDecimals { get; set; }
|
||||
}
|
||||
}
|
35
CryptoExchange.Net/CommonObjects/Ticker.cs
Normal file
35
CryptoExchange.Net/CommonObjects/Ticker.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticker data
|
||||
/// </summary>
|
||||
public class Ticker: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Price 24 hours ago
|
||||
/// </summary>
|
||||
public decimal? Price24H { get; set; }
|
||||
/// <summary>
|
||||
/// Last trade price
|
||||
/// </summary>
|
||||
public decimal? LastPrice { get; set; }
|
||||
/// <summary>
|
||||
/// 24 hour low price
|
||||
/// </summary>
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// 24 hour high price
|
||||
/// </summary>
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// 24 hour volume
|
||||
/// </summary>
|
||||
public decimal? Volume { get; set; }
|
||||
}
|
||||
}
|
50
CryptoExchange.Net/CommonObjects/Trade.cs
Normal file
50
CryptoExchange.Net/CommonObjects/Trade.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade data
|
||||
/// </summary>
|
||||
public class Trade: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol of the trade
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp of the trade
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User trade info
|
||||
/// </summary>
|
||||
public class UserTrade: Trade
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the trade
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Order id of the trade
|
||||
/// </summary>
|
||||
public string? OrderId { get; set; }
|
||||
/// <summary>
|
||||
/// Fee of the trade
|
||||
/// </summary>
|
||||
public decimal? Fee { get; set; }
|
||||
/// <summary>
|
||||
/// The asset the fee is paid in
|
||||
/// </summary>
|
||||
public string? FeeAsset { get; set; }
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters
|
||||
return ParseObject(arr, result, objectType);
|
||||
}
|
||||
|
||||
private static object? ParseObject(JArray arr, object result, Type objectType)
|
||||
private static object ParseObject(JArray arr, object result, Type objectType)
|
||||
{
|
||||
foreach (var property in objectType.GetProperties())
|
||||
{
|
||||
@ -63,8 +63,8 @@ namespace CryptoExchange.Net.Converters
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
|
||||
foreach (var obj in innerArray)
|
||||
{
|
||||
var innerObj = Activator.CreateInstance(objType);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType);
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
|
||||
count++;
|
||||
}
|
||||
property.SetValue(result, arrayResult);
|
||||
@ -72,8 +72,8 @@ namespace CryptoExchange.Net.Converters
|
||||
else
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||
var innerObj = Activator.CreateInstance(objType);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType);
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
continue;
|
||||
@ -181,6 +181,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <summary>
|
||||
/// Mark property as an index in the array
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ArrayPropertyAttribute: Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -43,9 +43,13 @@ namespace CryptoExchange.Net.Converters
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if (!GetValue(reader.Value.ToString(), out var result))
|
||||
var stringValue = reader.Value.ToString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (!GetValue(stringValue, out var result))
|
||||
{
|
||||
Debug.WriteLine($"Cannot map enum. Type: {typeof(T)}, Value: {reader.Value}");
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, 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 null;
|
||||
}
|
||||
|
||||
@ -71,7 +75,7 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
private bool GetValue(string value, out T result)
|
||||
{
|
||||
//check for exact match first, then if not found fallback to a case insensitive match
|
||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
||||
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if(mapping.Equals(default(KeyValuePair<T, string>)))
|
||||
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
193
CryptoExchange.Net/Converters/DateTimeConverter.cs
Normal file
193
CryptoExchange.Net/Converters/DateTimeConverter.cs
Normal file
@ -0,0 +1,193 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
||||
/// </summary>
|
||||
public class DateTimeConverter: JsonConverter
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if(reader.TokenType is JsonToken.Integer)
|
||||
{
|
||||
var longValue = (long)reader.Value;
|
||||
if (longValue == 0)
|
||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||
if (longValue < 1999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 1999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 1999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonToken.Float)
|
||||
{
|
||||
var doubleValue = (double)reader.Value;
|
||||
if (doubleValue < 1999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
|
||||
return ConvertFromMilliseconds(doubleValue);
|
||||
}
|
||||
else if(reader.TokenType is JsonToken.String)
|
||||
{
|
||||
var stringValue = (string)reader.Value;
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
// Parse 20211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 6)
|
||||
{
|
||||
// Parse 211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (doubleValue < 1999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
if (doubleValue < 1999999999999)
|
||||
return ConvertFromMilliseconds((long)doubleValue);
|
||||
if (doubleValue < 1999999999999999)
|
||||
return ConvertFromMicroseconds((long)doubleValue);
|
||||
|
||||
return ConvertFromNanoseconds((long)doubleValue);
|
||||
}
|
||||
|
||||
if(stringValue.Length == 10)
|
||||
{
|
||||
// Parse 2021-11-03 format
|
||||
var values = stringValue.Split('-');
|
||||
if(!int.TryParse(values[0], out var year)
|
||||
|| !int.TryParse(values[1], out var month)
|
||||
|| !int.TryParse(values[2], out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
else if(reader.TokenType == JsonToken.Date)
|
||||
{
|
||||
return (DateTime)reader.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="milliseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="microseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="nanoseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null: (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / ticksPerNanosecond);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var datetimeValue = (DateTime?)value;
|
||||
if (datetimeValue == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
if(datetimeValue == default(DateTime))
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
113
CryptoExchange.Net/Converters/EnumConverter.cs
Normal file
113
CryptoExchange.Net/Converters/EnumConverter.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||
/// </summary>
|
||||
public class EnumConverter : JsonConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsEnum;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var stringValue = reader.Value.ToString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (!GetValue(objectType, mapping, stringValue, out var result))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {objectType.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 null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
||||
{
|
||||
var mapping = new List<KeyValuePair<object, string>>();
|
||||
var enumMembers = objectType.GetMembers();
|
||||
foreach (var member in enumMembers)
|
||||
{
|
||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||
foreach (MapAttribute attribute in maps)
|
||||
{
|
||||
foreach (var value in attribute.Values)
|
||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
||||
}
|
||||
}
|
||||
_mapping.TryAdd(objectType, mapping);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
||||
{
|
||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
{
|
||||
result = mapping.Key;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = Enum.Parse(objectType, value, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
public static string? GetString<T>(T enumValue)
|
||||
{
|
||||
var objectType = typeof(T);
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = GetString(value);
|
||||
writer.WriteRawValue(stringValue);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// converter for milliseconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var t = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if(value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for nanoseconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampNanoSecondsConverter : JsonConverter
|
||||
{
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var nanoSeconds = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerNanosecond));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).Ticks / ticksPerNanosecond));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for seconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampSecondsConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if (reader.Value is double d)
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d);
|
||||
|
||||
var t = double.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture);
|
||||
// Set ticks instead of seconds or milliseconds, because AddSeconds/AddMilliseconds rounds to nearest millisecond
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)(t * TimeSpan.TicksPerSecond));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalSeconds));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// converter for datetime string (yyyymmdd) to datetime
|
||||
/// </summary>
|
||||
public class TimestampStringConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var value = reader.Value.ToString();
|
||||
if (value.Length == 8)
|
||||
return new DateTime(int.Parse(value.Substring(0, 4)), int.Parse(value.Substring(4, 2)), int.Parse(value.Substring(6, 2)), 0, 0, 0, DateTimeKind.Utc);
|
||||
else if(value.Length == 6)
|
||||
return new DateTime(int.Parse(value.Substring(0, 2)), int.Parse(value.Substring(2, 2)), int.Parse(value.Substring(4, 2)), 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
throw new Exception("Unknown datetime value: " + value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
{
|
||||
var dateTimeValue = (DateTime)value;
|
||||
writer.WriteValue(int.Parse($"{dateTimeValue.Year}{dateTimeValue.Month}{dateTimeValue.Day}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for utc datetime
|
||||
/// </summary>
|
||||
public class UTCDateTimeConverter: JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(JsonConvert.SerializeObject(value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
DateTime value;
|
||||
if (reader.Value is string s)
|
||||
value = (DateTime)JsonConvert.DeserializeObject(s)!;
|
||||
else
|
||||
value = (DateTime) reader.Value;
|
||||
|
||||
return DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,19 +5,19 @@
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency exchange API's</Description>
|
||||
<PackageVersion>4.2.8</PackageVersion>
|
||||
<AssemblyVersion>4.2.8</AssemblyVersion>
|
||||
<FileVersion>4.2.8</FileVersion>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>5.0.0</PackageVersion>
|
||||
<AssemblyVersion>5.0.0</AssemblyVersion>
|
||||
<FileVersion>5.0.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>4.2.8 - Fixed deadlock in socket receive, Fixed issue in reconnection handling when the client is disconnected again during resubscribing, Added some additional checking of socket state to prevent sending/expecting data when socket is not connected</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>See https://github.com/JKorf/CryptoExchange.Net#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
@ -41,11 +41,14 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[3.1.0,)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="[3.1.0,)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common balance
|
||||
/// </summary>
|
||||
public interface ICommonBalance
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset name
|
||||
/// </summary>
|
||||
public string CommonAsset { get; }
|
||||
/// <summary>
|
||||
/// Amount available
|
||||
/// </summary>
|
||||
public decimal CommonAvailable { get; }
|
||||
/// <summary>
|
||||
/// Total amount
|
||||
/// </summary>
|
||||
public decimal CommonTotal { get; }
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common trade
|
||||
/// </summary>
|
||||
public interface ICommonTrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the trade
|
||||
/// </summary>
|
||||
public string CommonId { get; }
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
public decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
public decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Fee paid for the trade
|
||||
/// </summary>
|
||||
public decimal CommonFee { get; }
|
||||
/// <summary>
|
||||
/// The asset fee was paid in
|
||||
/// </summary>
|
||||
public string? CommonFeeAsset { get; }
|
||||
/// <summary>
|
||||
/// Trade time
|
||||
/// </summary>
|
||||
DateTime CommonTradeTime { get; }
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common kline
|
||||
/// </summary>
|
||||
public interface ICommonKline
|
||||
{
|
||||
/// <summary>
|
||||
/// High price for this kline
|
||||
/// </summary>
|
||||
decimal CommonHigh { get; }
|
||||
/// <summary>
|
||||
/// Low price for this kline
|
||||
/// </summary>
|
||||
decimal CommonLow { get; }
|
||||
/// <summary>
|
||||
/// Open price for this kline
|
||||
/// </summary>
|
||||
decimal CommonOpen { get; }
|
||||
/// <summary>
|
||||
/// Close price for this kline
|
||||
/// </summary>
|
||||
decimal CommonClose { get; }
|
||||
/// <summary>
|
||||
/// Open time for this kline
|
||||
/// </summary>
|
||||
DateTime CommonOpenTime { get; }
|
||||
/// <summary>
|
||||
/// Volume of this kline
|
||||
/// </summary>
|
||||
decimal CommonVolume { get; }
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common order
|
||||
/// </summary>
|
||||
public interface ICommonOrder: ICommonOrderId
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol of the order
|
||||
/// </summary>
|
||||
public string CommonSymbol { get; }
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
public decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Status of the order
|
||||
/// </summary>
|
||||
public IExchangeClient.OrderStatus CommonStatus { get; }
|
||||
/// <summary>
|
||||
/// Whether the order is active
|
||||
/// </summary>
|
||||
public bool IsActive { get; }
|
||||
/// <summary>
|
||||
/// Side of the order
|
||||
/// </summary>
|
||||
public IExchangeClient.OrderSide CommonSide { get; }
|
||||
/// <summary>
|
||||
/// Type of the order
|
||||
/// </summary>
|
||||
public IExchangeClient.OrderType CommonType { get; }
|
||||
/// <summary>
|
||||
/// order time
|
||||
/// </summary>
|
||||
DateTime CommonOrderTime { get; }
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common order book
|
||||
/// </summary>
|
||||
public interface ICommonOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Bids
|
||||
/// </summary>
|
||||
IEnumerable<ISymbolOrderBookEntry> CommonBids { get; }
|
||||
/// <summary>
|
||||
/// Asks
|
||||
/// </summary>
|
||||
IEnumerable<ISymbolOrderBookEntry> CommonAsks { get; }
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common order id
|
||||
/// </summary>
|
||||
public interface ICommonOrderId
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the order
|
||||
/// </summary>
|
||||
public string CommonId { get; }
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Recent trade
|
||||
/// </summary>
|
||||
public interface ICommonRecentTrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Trade time
|
||||
/// </summary>
|
||||
DateTime CommonTradeTime { get; }
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common symbol
|
||||
/// </summary>
|
||||
public interface ICommonSymbol
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
public string CommonName { get; }
|
||||
/// <summary>
|
||||
/// Minimum trade size
|
||||
/// </summary>
|
||||
public decimal CommonMinimumTradeSize { get; }
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common ticker
|
||||
/// </summary>
|
||||
public interface ICommonTicker
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
public string CommonSymbol { get; }
|
||||
/// <summary>
|
||||
/// High price
|
||||
/// </summary>
|
||||
public decimal CommonHigh { get; }
|
||||
/// <summary>
|
||||
/// Low price
|
||||
/// </summary>
|
||||
public decimal CommonLow { get; }
|
||||
/// <summary>
|
||||
/// Volume
|
||||
/// </summary>
|
||||
public decimal CommonVolume { get; }
|
||||
}
|
||||
}
|
@ -5,8 +5,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -74,7 +73,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
||||
{
|
||||
if(value != null)
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
@ -129,7 +128,7 @@ namespace CryptoExchange.Net
|
||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||
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}"))}&";
|
||||
else
|
||||
{
|
||||
@ -144,6 +143,29 @@ namespace CryptoExchange.Net
|
||||
return uriString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary to formdata string
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFormData(this SortedDictionary<string, object> parameters)
|
||||
{
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
foreach (var value in array)
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
}
|
||||
else
|
||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
return formData.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the string the secure string is representing
|
||||
/// </summary>
|
||||
@ -177,6 +199,41 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Are 2 secure strings equal
|
||||
/// </summary>
|
||||
/// <param name="ss1">Source secure string</param>
|
||||
/// <param name="ss2">Compare secure string</param>
|
||||
/// <returns>True if equal by value</returns>
|
||||
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
|
||||
{
|
||||
IntPtr bstr1 = IntPtr.Zero;
|
||||
IntPtr bstr2 = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
bstr1 = Marshal.SecureStringToBSTR(ss1);
|
||||
bstr2 = Marshal.SecureStringToBSTR(ss2);
|
||||
int length1 = Marshal.ReadInt32(bstr1, -4);
|
||||
int length2 = Marshal.ReadInt32(bstr2, -4);
|
||||
if (length1 == length2)
|
||||
{
|
||||
for (int x = 0; x < length1; ++x)
|
||||
{
|
||||
byte b1 = Marshal.ReadByte(bstr1, x);
|
||||
byte b2 = Marshal.ReadByte(bstr2, x);
|
||||
if (b1 != b2) return false;
|
||||
}
|
||||
}
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
|
||||
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a secure string from a string
|
||||
/// </summary>
|
||||
@ -210,14 +267,14 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Debug.WriteLine(info);
|
||||
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Debug.WriteLine(info);
|
||||
if (log == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -298,7 +355,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToLogString(this Exception exception)
|
||||
public static string ToLogString(this Exception? exception)
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
var indent = 0;
|
||||
@ -319,6 +376,103 @@ namespace CryptoExchange.Net
|
||||
|
||||
return message.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append a base url with provided path
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string AppendPath(this string url, params string[] path)
|
||||
{
|
||||
if (!url.EndsWith("/"))
|
||||
url += "/";
|
||||
|
||||
foreach (var item in path)
|
||||
url += item.Trim('/') + "/";
|
||||
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
public static string FillPathParameters(this string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="baseUri"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="baseUri"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add parameter to URI
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||
{
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
httpValueCollection.Remove(name);
|
||||
httpValueCollection.Add(name, value);
|
||||
|
||||
var ub = new UriBuilder(uri);
|
||||
ub.Query = httpValueCollection.ToString();
|
||||
|
||||
return ub.Uri;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,50 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared interface for exchange wrappers based on the CryptoExchange.Net package
|
||||
/// Common rest client endpoints
|
||||
/// </summary>
|
||||
public interface IExchangeClient
|
||||
public interface IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the exchange
|
||||
/// </summary>
|
||||
string ExchangeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Should be triggered on order placing
|
||||
/// </summary>
|
||||
event Action<ICommonOrderId> OnOrderPlaced;
|
||||
event Action<OrderId> OnOrderPlaced;
|
||||
/// <summary>
|
||||
/// Should be triggered on order cancelling
|
||||
/// </summary>
|
||||
event Action<ICommonOrderId> OnOrderCanceled;
|
||||
event Action<OrderId> OnOrderCanceled;
|
||||
|
||||
/// <summary>
|
||||
/// Get the symbol name based on a base and quote asset
|
||||
/// </summary>
|
||||
/// <param name="baseAsset"></param>
|
||||
/// <param name="quoteAsset"></param>
|
||||
/// <param name="baseAsset">The base asset</param>
|
||||
/// <param name="quoteAsset">The quote asset</param>
|
||||
/// <returns></returns>
|
||||
string GetSymbolName(string baseAsset, string quoteAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of symbols for the exchange
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonSymbol>>> GetSymbolsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of tickers for the exchange
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonTicker>>> GetTickersAsync();
|
||||
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a ticker for the exchange
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get klines for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonTicker>> GetTickerAsync(string symbol);
|
||||
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of tickers for the exchange
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of candles for a given symbol on the exchange
|
||||
@ -54,124 +64,75 @@ namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
|
||||
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
|
||||
/// <param name="limit">[Optional] Max number of results</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonKline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
|
||||
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the order book for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the book for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrderBook>> GetOrderBookAsync(string symbol);
|
||||
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// The recent trades for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the trades for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonRecentTrade>>> GetRecentTradesAsync(string symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<ICommonOrderId>> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal quantity, decimal? price = null, string? accountId = null);
|
||||
/// <summary>
|
||||
/// Get an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrder>> GetOrderAsync(string orderId, string? symbol = null);
|
||||
/// <summary>
|
||||
/// Get trades for an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonTrade>>> GetTradesAsync(string orderId, string? symbol = null);
|
||||
/// <summary>
|
||||
/// Get a list of open orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetOpenOrdersAsync(string? symbol = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of closed orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetClosedOrdersAsync(string? symbol = null);
|
||||
/// <summary>
|
||||
/// Cancel an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrderId>> CancelOrderAsync(string orderId, string? symbol = null);
|
||||
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get balances
|
||||
/// </summary>
|
||||
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonBalance>>> GetBalancesAsync(string? accountId = null);
|
||||
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Common order id
|
||||
/// Get an order by id
|
||||
/// </summary>
|
||||
public enum OrderType
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit type
|
||||
/// </summary>
|
||||
Limit,
|
||||
/// <summary>
|
||||
/// Market type
|
||||
/// </summary>
|
||||
Market,
|
||||
/// <summary>
|
||||
/// Other order type
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Common order side
|
||||
/// Get trades for an order by id
|
||||
/// </summary>
|
||||
public enum OrderSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Buy order
|
||||
/// </summary>
|
||||
Buy,
|
||||
/// <summary>
|
||||
/// Sell order
|
||||
/// </summary>
|
||||
Sell
|
||||
}
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Common order status
|
||||
/// Get a list of open orders
|
||||
/// </summary>
|
||||
public enum OrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// placed and not fully filled order
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// cancelled order
|
||||
/// </summary>
|
||||
Canceled,
|
||||
/// <summary>
|
||||
/// filled order
|
||||
/// </summary>
|
||||
Filled
|
||||
}
|
||||
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of closed orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common futures endpoints
|
||||
/// </summary>
|
||||
public interface IFuturesClient : IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="leverage">[Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand)</param>
|
||||
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get position
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
28
CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs
Normal file
28
CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common spot endpoints
|
||||
/// </summary>
|
||||
public interface ISpotClient: IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
@ -1,4 +1,9 @@
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@ -8,13 +13,17 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface IRateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit the request if needed
|
||||
/// Limit a request based on previous requests made
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="limitBehaviour"></param>
|
||||
/// <param name="credits"></param>
|
||||
/// <returns></returns>
|
||||
CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits=1);
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="endpoint">The endpoint the request is for</param>
|
||||
/// <param name="method">The Http request method</param>
|
||||
/// <param name="signed">Whether the request is singed(private) or not</param>
|
||||
/// <param name="apiKey">The api key making this request</param>
|
||||
/// <param name="limitBehaviour">The limit behavior for when the limit is reached</param>
|
||||
/// <param name="requestWeight">The weight of the request</param>
|
||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||
/// <returns>The time in milliseconds spend waiting</returns>
|
||||
Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <returns></returns>
|
||||
IRequest Create(HttpMethod method, string uri, int requestId);
|
||||
IRequest Create(HttpMethod method, Uri uri, int requestId);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the requests created by this factory
|
||||
|
@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@ -18,51 +15,19 @@ namespace CryptoExchange.Net.Interfaces
|
||||
IRequestFactory RequestFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What should happen when hitting a rate limit
|
||||
/// </summary>
|
||||
RateLimitingBehaviour RateLimitBehaviour { get; }
|
||||
|
||||
/// <summary>
|
||||
/// List of active rate limiters
|
||||
/// </summary>
|
||||
IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of requests made
|
||||
/// The total amount of requests made with this client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the API
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
BaseRestClientOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
string ExchangeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="limiter">The limiter to add</param>
|
||||
void AddRateLimiter(IRateLimiter limiter);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all rate limiters from this client
|
||||
/// </summary>
|
||||
void RemoveRateLimiters();
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
CallResult<long> Ping(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
Task<CallResult<long>> PingAsync(CancellationToken ct = default);
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
void SetApiCredentials(ApiCredentials credentials);
|
||||
}
|
||||
}
|
@ -11,48 +11,21 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface ISocketClient: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
BaseSocketClientOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time in between reconnect attempts
|
||||
/// Incoming kilobytes per second of data
|
||||
/// </summary>
|
||||
TimeSpan ReconnectInterval { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the client should try to auto reconnect when losing connection
|
||||
/// </summary>
|
||||
bool AutoReconnect { get; }
|
||||
public double IncomingKbps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the API
|
||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
|
||||
TimeSpan ResponseTimeout { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
|
||||
TimeSpan SocketNoDataTimeout { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of concurrent socket connections
|
||||
/// </summary>
|
||||
int MaxSocketConnections { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
|
||||
int SocketCombineTarget { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxReconnectTries"/>
|
||||
int? MaxReconnectTries { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxResubscribeTries"/>
|
||||
int? MaxResubscribeTries { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxConcurrentResubscriptionsPerSocket"/>
|
||||
int MaxConcurrentResubscriptionsPerSocket { get; }
|
||||
/// <summary>
|
||||
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAsync(int subscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from a stream
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
@ -10,6 +11,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
public interface ISymbolOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The status of the order book. Order book is up to date when the status is `Synced`
|
||||
/// </summary>
|
||||
@ -39,7 +45,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Timestamp of the last update
|
||||
/// </summary>
|
||||
DateTime LastOrderBookUpdate { get; }
|
||||
DateTime UpdateTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of asks in the book
|
||||
@ -83,8 +89,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Start connecting and synchronizing the order book
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<bool>> StartAsync();
|
||||
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
|
||||
|
||||
/// <summary>
|
||||
/// Stop syncing the order book
|
||||
|
@ -7,41 +7,41 @@ using System.Threading.Tasks;
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for websocket interaction
|
||||
/// Webscoket connection interface
|
||||
/// </summary>
|
||||
public interface IWebsocket: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Websocket closed
|
||||
/// Websocket closed event
|
||||
/// </summary>
|
||||
event Action OnClose;
|
||||
/// <summary>
|
||||
/// Websocket message received
|
||||
/// Websocket message received event
|
||||
/// </summary>
|
||||
event Action<string> OnMessage;
|
||||
/// <summary>
|
||||
/// Websocket error
|
||||
/// Websocket error event
|
||||
/// </summary>
|
||||
event Action<Exception> OnError;
|
||||
/// <summary>
|
||||
/// Websocket opened
|
||||
/// Websocket opened event
|
||||
/// </summary>
|
||||
event Action OnOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// Unique id for this socket
|
||||
/// </summary>
|
||||
int Id { get; }
|
||||
/// <summary>
|
||||
/// Origin
|
||||
/// Origin header
|
||||
/// </summary>
|
||||
string? Origin { get; set; }
|
||||
/// <summary>
|
||||
/// Encoding to use
|
||||
/// Encoding to use for sending/receiving string data
|
||||
/// </summary>
|
||||
Encoding? Encoding { get; set; }
|
||||
/// <summary>
|
||||
/// Reconnecting
|
||||
/// Whether socket is in the process of reconnecting
|
||||
/// </summary>
|
||||
bool Reconnecting { get; set; }
|
||||
/// <summary>
|
||||
@ -61,15 +61,15 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
Func<string, string>? DataInterpreterString { get; set; }
|
||||
/// <summary>
|
||||
/// Socket url
|
||||
/// The url the socket connects to
|
||||
/// </summary>
|
||||
string Url { get; }
|
||||
/// <summary>
|
||||
/// Is closed
|
||||
/// Whether the socket connection is closed
|
||||
/// </summary>
|
||||
bool IsClosed { get; }
|
||||
/// <summary>
|
||||
/// Is open
|
||||
/// Whether the socket connection is open
|
||||
/// </summary>
|
||||
bool IsOpen { get; }
|
||||
/// <summary>
|
||||
@ -77,10 +77,15 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
SslProtocols SSLProtocols { get; set; }
|
||||
/// <summary>
|
||||
/// Timeout
|
||||
/// The max time for no data being received before the connection is considered lost
|
||||
/// </summary>
|
||||
TimeSpan Timeout { get; set; }
|
||||
/// <summary>
|
||||
/// Set a proxy to use when connecting
|
||||
/// </summary>
|
||||
/// <param name="proxy"></param>
|
||||
void SetProxy(ApiProxy proxy);
|
||||
/// <summary>
|
||||
/// Connect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
@ -91,18 +96,13 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="data"></param>
|
||||
void Send(string data);
|
||||
/// <summary>
|
||||
/// Reset socket
|
||||
/// Reset socket when a connection is lost to prepare for a new connection
|
||||
/// </summary>
|
||||
void Reset();
|
||||
/// <summary>
|
||||
/// Close the connecting
|
||||
/// Close the connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task CloseAsync();
|
||||
/// <summary>
|
||||
/// Set proxy
|
||||
/// </summary>
|
||||
/// <param name="proxy"></param>
|
||||
void SetProxy(ApiProxy proxy);
|
||||
}
|
||||
}
|
||||
|
@ -11,17 +11,17 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="url">The url the socket is fo</param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(Log log, string url);
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="cookies"></param>
|
||||
/// <param name="headers"></param>
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="url">The url the socket is fo</param>
|
||||
/// <param name="cookies">Cookies to be send in the initial request</param>
|
||||
/// <param name="headers">Headers to be send in the initial request</param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ using System;
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Log to console
|
||||
/// ILogger implementation for logging to the console
|
||||
/// </summary>
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Logging
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
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);
|
||||
|
@ -5,7 +5,7 @@ using System.Diagnostics;
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Default log writer, writes to debug
|
||||
/// Default log writer, uses Trace.WriteLine
|
||||
/// </summary>
|
||||
public class DebugLogger: ILogger
|
||||
{
|
||||
@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Logging
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
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);
|
||||
|
@ -65,7 +65,7 @@ namespace CryptoExchange.Net.Logging
|
||||
catch (Exception e)
|
||||
{
|
||||
// Can't write to the logging so where else to output..
|
||||
Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -12,10 +11,10 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class AsyncResetEvent : IDisposable
|
||||
{
|
||||
private readonly static 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 bool _signaled;
|
||||
private bool _reset;
|
||||
private readonly bool _reset;
|
||||
|
||||
/// <summary>
|
||||
/// New AsyncResetEvent
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@ -36,16 +38,6 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult CreateErrorResult(Error error)
|
||||
{
|
||||
return new WebCallResult(null, null, error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -62,22 +54,36 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
public string? OriginalData { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="error"></param>
|
||||
#pragma warning disable 8618
|
||||
public CallResult([AllowNull]T data, Error? error): base(error)
|
||||
protected CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
|
||||
#pragma warning restore 8618
|
||||
{
|
||||
OriginalData = originalData;
|
||||
#pragma warning disable 8601
|
||||
Data = data;
|
||||
#pragma warning restore 8601
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new data result
|
||||
/// </summary>
|
||||
/// <param name="data">The data to return</param>
|
||||
public CallResult(T data) : this(data, null, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The erro rto return</param>
|
||||
public CallResult(Error error) : this(default, null, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
|
||||
/// </summary>
|
||||
@ -111,16 +117,6 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
public new static WebCallResult<T> CreateErrorResult(Error error)
|
||||
{
|
||||
return new WebCallResult<T>(null, null, default, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@ -129,7 +125,18 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public CallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new CallResult<K>(data, Error);
|
||||
return new CallResult<K>(data, OriginalData, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error to return</param>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,6 +145,26 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class WebCallResult : CallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
@ -148,40 +175,56 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code">Status code</param>
|
||||
/// <param name="responseHeaders">Response headers</param>
|
||||
/// <param name="error">Error</param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
/// <param name="requestHeaders"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error? error) : base(error)
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||
Error? error) : base(error)
|
||||
{
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code">Status code</param>
|
||||
/// <param name="responseHeaders">Response headers</param>
|
||||
/// <param name="error">Error</param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
|
||||
{
|
||||
return new WebCallResult(code, responseHeaders, error);
|
||||
}
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(Error error): base(error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// Return the result as an error result
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult CreateErrorResult(WebCallResult result)
|
||||
public WebCallResult AsError(Error error)
|
||||
{
|
||||
return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, result.Error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,6 +234,26 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class WebCallResult<T>: CallResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
@ -200,44 +263,53 @@ namespace CryptoExchange.Net.Objects
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
[AllowNull] T data,
|
||||
Error? error): base(data, error)
|
||||
{
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new result
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
/// <param name="requestHeaders"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? originalData,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, error)
|
||||
Error? error) : base(data, originalData, error)
|
||||
{
|
||||
OriginalData = originalData;
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@ -246,19 +318,36 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, OriginalData, data, Error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult<T> CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
|
||||
public WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult<T>(code, responseHeaders, default, error);
|
||||
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>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
@ -9,14 +10,14 @@ using Microsoft.Extensions.Logging;
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base options
|
||||
/// Base options, applicable to everything
|
||||
/// </summary>
|
||||
public class BaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers.
|
||||
/// The minimum log level to output
|
||||
/// </summary>
|
||||
public LogLevel? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information;
|
||||
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// The log writers
|
||||
@ -28,6 +29,19 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public void Copy<T>(T input, T def) where T : BaseOptions
|
||||
{
|
||||
input.LogLevel = def.LogLevel;
|
||||
input.LogWriters = def.LogWriters.ToList();
|
||||
input.OutputOriginalData = def.OutputOriginalData;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
@ -36,184 +50,81 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// Client options, for both the socket and rest clients
|
||||
/// </summary>
|
||||
public class OrderBookOptions : BaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the order book implementation
|
||||
/// </summary>
|
||||
public string OrderBookName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.
|
||||
/// </summary>
|
||||
public bool SequenceNumbersAreConsecutive { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
|
||||
/// when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed
|
||||
/// </summary>
|
||||
public bool StrictLevels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the order book implementation</param>
|
||||
/// <param name="sequencesAreConsecutive">Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.</param>
|
||||
/// <param name="strictLevels">Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
|
||||
/// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed</param>
|
||||
public OrderBookOptions(string name, bool sequencesAreConsecutive, bool strictLevels)
|
||||
{
|
||||
OrderBookName = name;
|
||||
SequenceNumbersAreConsecutive = sequencesAreConsecutive;
|
||||
StrictLevels = strictLevels;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}, StrictLevels: {StrictLevels}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base client options
|
||||
/// </summary>
|
||||
public class ClientOptions : BaseOptions
|
||||
public class BaseClientOptions : BaseOptions
|
||||
{
|
||||
private string _baseAddress;
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress
|
||||
{
|
||||
get => _baseAddress;
|
||||
set
|
||||
{
|
||||
var newValue = value;
|
||||
if (!newValue.EndsWith("/"))
|
||||
newValue += "/";
|
||||
_baseAddress = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should check objects for missing properties based on the model and the received JSON
|
||||
/// </summary>
|
||||
public bool ShouldCheckObjects { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy to use
|
||||
/// Proxy to use when connecting
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// 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>
|
||||
/// <param name="baseAddress">The base address to use</param>
|
||||
#pragma warning disable 8618
|
||||
public ClientOptions(string baseAddress)
|
||||
#pragma warning restore 8618
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public new void Copy<T>(T input, T def) where T : BaseClientOptions
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
base.Copy(input, def);
|
||||
|
||||
input.Proxy = def.Proxy;
|
||||
input.ApiCredentials = def.ApiCredentials?.Copy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
|
||||
return $"{base.ToString()}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}, Base.ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for rest client options
|
||||
/// Rest client options
|
||||
/// </summary>
|
||||
public class RestClientOptions : ClientOptions
|
||||
public class BaseRestClientOptions : BaseClientOptions
|
||||
{
|
||||
/// <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>
|
||||
/// 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 will be ignored in requests and should be set on the provided HttpClient instance
|
||||
/// 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>
|
||||
/// <param name="baseAddress">The base address of the API</param>
|
||||
public RestClientOptions(string baseAddress): base(baseAddress)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address of the API</param>
|
||||
/// <param name="httpClient">Shared http client instance</param>
|
||||
public RestClientOptions(HttpClient httpClient, string baseAddress) : base(baseAddress)
|
||||
{
|
||||
HttpClient = httpClient;
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a copy of the options
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestClientOptions, new()
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public new void Copy<T>(T input, T def) where T : BaseRestClientOptions
|
||||
{
|
||||
var copy = new T
|
||||
{
|
||||
BaseAddress = BaseAddress,
|
||||
LogLevel = LogLevel,
|
||||
Proxy = Proxy,
|
||||
LogWriters = LogWriters,
|
||||
RateLimiters = RateLimiters,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
RequestTimeout = RequestTimeout,
|
||||
HttpClient = HttpClient
|
||||
};
|
||||
base.Copy(input, def);
|
||||
|
||||
if (ApiCredentials != null)
|
||||
copy.ApiCredentials = ApiCredentials.Copy();
|
||||
|
||||
return copy;
|
||||
input.HttpClient = def.HttpClient;
|
||||
input.RequestTimeout = def.RequestTimeout;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}";
|
||||
return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for socket client options
|
||||
/// Socket client options
|
||||
/// </summary>
|
||||
public class SocketClientOptions : ClientOptions
|
||||
public class BaseSocketClientOptions : BaseClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
@ -226,7 +137,7 @@ namespace CryptoExchange.Net.Objects
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of times to try to reconnect
|
||||
/// The maximum number of times to try to reconnect, default null will retry indefinitely
|
||||
/// </summary>
|
||||
public int? MaxReconnectTries { get; set; }
|
||||
|
||||
@ -241,58 +152,196 @@ namespace CryptoExchange.Net.Objects
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait for a socket response before giving a timeout
|
||||
/// 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 time 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.
|
||||
/// 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 exchanges support multiple subscriptions on a single socket.
|
||||
/// 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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address to use</param>
|
||||
public SocketClientOptions(string baseAddress) : base(baseAddress)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of the options
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketClientOptions, new()
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public new void Copy<T>(T input, T def) where T : BaseSocketClientOptions
|
||||
{
|
||||
var copy = new T
|
||||
{
|
||||
BaseAddress = BaseAddress,
|
||||
LogLevel = LogLevel,
|
||||
Proxy = Proxy,
|
||||
LogWriters = LogWriters,
|
||||
AutoReconnect = AutoReconnect,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketResponseTimeout = SocketResponseTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget
|
||||
};
|
||||
|
||||
if (ApiCredentials != null)
|
||||
copy.ApiCredentials = ApiCredentials.Copy();
|
||||
|
||||
return copy;
|
||||
base.Copy(input, def);
|
||||
|
||||
input.AutoReconnect = def.AutoReconnect;
|
||||
input.ReconnectInterval = def.ReconnectInterval;
|
||||
input.MaxReconnectTries = def.MaxReconnectTries;
|
||||
input.MaxResubscribeTries = def.MaxResubscribeTries;
|
||||
input.MaxConcurrentResubscriptionsPerSocket = def.MaxConcurrentResubscriptionsPerSocket;
|
||||
input.SocketResponseTimeout = def.SocketResponseTimeout;
|
||||
input.SocketNoDataTimeout = def.SocketNoDataTimeout;
|
||||
input.SocketSubscriptionsCombineTarget = def.SocketSubscriptionsCombineTarget;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxReconnectTries: {MaxReconnectTries}, MaxResubscribeTries: {MaxResubscribeTries}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <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="baseOn">Copy values for the provided options</param>
|
||||
#pragma warning disable 8618 // Will always get filled by the provided options
|
||||
public ApiClientOptions(ApiClientOptions baseOn)
|
||||
{
|
||||
Copy(this, baseOn);
|
||||
}
|
||||
#pragma warning restore 8618
|
||||
|
||||
/// <summary>
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public void Copy<T>(T input, T def) where T : ApiClientOptions
|
||||
{
|
||||
if (def.BaseAddress != null)
|
||||
input.BaseAddress = def.BaseAddress;
|
||||
input.ApiCredentials = def.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>
|
||||
public RestApiClientOptions(RestApiClientOptions baseOn)
|
||||
{
|
||||
Copy(this, baseOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the values of the def to the input
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="input"></param>
|
||||
/// <param name="def"></param>
|
||||
public new void Copy<T>(T input, T def) where T : RestApiClientOptions
|
||||
{
|
||||
base.Copy(input, def);
|
||||
|
||||
if(def.RateLimiters != null)
|
||||
input.RateLimiters = def.RateLimiters.ToList();
|
||||
input.RateLimitingBehaviour = def.RateLimitingBehaviour;
|
||||
input.AutoTimestamp = def.AutoTimestamp;
|
||||
input.TimestampRecalculationInterval = def.TimestampRecalculationInterval;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
}
|
||||
|
404
CryptoExchange.Net/Objects/RateLimiter.cs
Normal file
404
CryptoExchange.Net/Objects/RateLimiter.cs
Normal file
@ -0,0 +1,404 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits the amount of requests to a certain constraint
|
||||
/// </summary>
|
||||
public class RateLimiter : IRateLimiter
|
||||
{
|
||||
private readonly object _limiterLock = new object();
|
||||
internal List<Limiter> Limiters = new List<Limiter>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
||||
/// <see cref="AddEndpointLimit(string, int, TimeSpan, HttpMethod?, bool)"/>, <see cref="AddPartialEndpointLimit(string, int, TimeSpan, HttpMethod?, bool, bool)"/> or <see cref="AddApiKeyLimit"/>.
|
||||
/// </summary>
|
||||
public RateLimiter()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit for the total amount of requests per time period
|
||||
/// </summary>
|
||||
/// <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 AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint 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>
|
||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
||||
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
|
||||
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The endpoints 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>
|
||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
||||
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
|
||||
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint 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>
|
||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
||||
/// <param name="ignoreOtherRateLimits">If set to true it ignores other rate limits</param>
|
||||
/// <param name="countPerEndpoint">Whether all requests for this partial endpoint are bound to the same limit or each individual endpoint has its own limit</param>
|
||||
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit for the amount of requests per Api key
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="onlyForSignedRequests">Only include calls that are signed in this limiter</param>
|
||||
/// <param name="excludeFromTotalRateLimit">Exclude requests with API key from the total rate limiter</param>
|
||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult<int>> LimitRequestAsync(Log log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
||||
{
|
||||
int totalWaitTime = 0;
|
||||
|
||||
EndpointRateLimiter? endpointLimit;
|
||||
lock (_limiterLock)
|
||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||
if(endpointLimit != null)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
if (endpointLimit?.IgnoreOtherRateLimits == true)
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||
lock (_limiterLock)
|
||||
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
||||
foreach (var partialEndpointLimit in partialEndpointLimits)
|
||||
{
|
||||
if (partialEndpointLimit.CountPerEndpoint)
|
||||
{
|
||||
SingleTopicRateLimiter? thisEndpointLimit;
|
||||
lock (_limiterLock)
|
||||
{
|
||||
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
||||
if (thisEndpointLimit == null)
|
||||
{
|
||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
||||
Limiters.Add(thisEndpointLimit);
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(log, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
ApiKeyRateLimiter? apiLimit;
|
||||
lock (_limiterLock)
|
||||
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
||||
if (apiLimit != null)
|
||||
{
|
||||
if(apiKey == null)
|
||||
{
|
||||
if (!apiLimit.OnlyForSignedRequests)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
}
|
||||
else if (signed || !apiLimit.OnlyForSignedRequests)
|
||||
{
|
||||
SingleTopicRateLimiter? thisApiLimit;
|
||||
lock (_limiterLock)
|
||||
{
|
||||
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
||||
if (thisApiLimit == null)
|
||||
{
|
||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
||||
Limiters.Add(thisApiLimit);
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(log, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
TotalRateLimiter? totalLimit;
|
||||
lock (_limiterLock)
|
||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||
if (totalLimit != null)
|
||||
{
|
||||
var waitResult = await ProcessTopic(log, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
|
||||
private static async Task<CallResult<int>> ProcessTopic(Log log, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await historyTopic.Semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
int totalWaitTime = 0;
|
||||
while (true)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||
{
|
||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||
{
|
||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||
i--;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var currentWeight = historyTopic.Entries.Sum(h => h.Weight);
|
||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||
{
|
||||
// Wait until the next entry should be removed from the history
|
||||
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
|
||||
if (thisWaitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
historyTopic.Semaphore.Release();
|
||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||
log.Write(LogLevel.Warning, msg);
|
||||
return new CallResult<int>(new RateLimitError(msg));
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
totalWaitTime += thisWaitTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var newTime = DateTime.UtcNow;
|
||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||
historyTopic.Semaphore.Release();
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
|
||||
internal struct LimitEntry
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public int Weight { get; set; }
|
||||
|
||||
public LimitEntry(DateTime timestamp, int weight)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
Weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
internal class Limiter
|
||||
{
|
||||
public RateLimitType Type { get; set; }
|
||||
public HttpMethod? Method { get; set; }
|
||||
|
||||
public SemaphoreSlim Semaphore { get; set; }
|
||||
public int Limit { get; set; }
|
||||
|
||||
public TimeSpan Period { get; set; }
|
||||
public List<LimitEntry> Entries { get; set; } = new List<LimitEntry>();
|
||||
|
||||
public Limiter(RateLimitType type, int limit, TimeSpan perPeriod, HttpMethod? method)
|
||||
{
|
||||
Semaphore = new SemaphoreSlim(1, 1);
|
||||
Type = type;
|
||||
Limit = limit;
|
||||
Period = perPeriod;
|
||||
Method = method;
|
||||
}
|
||||
}
|
||||
|
||||
internal class TotalRateLimiter : Limiter
|
||||
{
|
||||
public TotalRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method)
|
||||
: base(RateLimitType.Total, limit, perPeriod, method)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(TotalRateLimiter);
|
||||
}
|
||||
}
|
||||
|
||||
internal class EndpointRateLimiter: Limiter
|
||||
{
|
||||
public string[] Endpoints { get; set; }
|
||||
public bool IgnoreOtherRateLimits { get; set; }
|
||||
|
||||
public EndpointRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits)
|
||||
:base(RateLimitType.Endpoint, limit, perPeriod, method)
|
||||
{
|
||||
Endpoints = endpoints;
|
||||
IgnoreOtherRateLimits = ignoreOtherRateLimits;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(EndpointRateLimiter) + $": {string.Join(", ", Endpoints)}";
|
||||
}
|
||||
}
|
||||
|
||||
internal class PartialEndpointRateLimiter : Limiter
|
||||
{
|
||||
public string[] PartialEndpoints { get; set; }
|
||||
public bool IgnoreOtherRateLimits { get; set; }
|
||||
public bool CountPerEndpoint { get; set; }
|
||||
|
||||
public PartialEndpointRateLimiter(string[] partialEndpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits, bool countPerEndpoint)
|
||||
: base(RateLimitType.PartialEndpoint, limit, perPeriod, method)
|
||||
{
|
||||
PartialEndpoints = partialEndpoints;
|
||||
IgnoreOtherRateLimits = ignoreOtherRateLimits;
|
||||
CountPerEndpoint = countPerEndpoint;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(PartialEndpointRateLimiter) + $": {string.Join(", ", PartialEndpoints)}";
|
||||
}
|
||||
}
|
||||
|
||||
internal class ApiKeyRateLimiter : Limiter
|
||||
{
|
||||
public bool OnlyForSignedRequests { get; set; }
|
||||
public bool IgnoreTotalRateLimit { get; set; }
|
||||
|
||||
public ApiKeyRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method, bool onlyForSignedRequests, bool ignoreTotalRateLimit)
|
||||
:base(RateLimitType.ApiKey, limit, perPeriod, method)
|
||||
{
|
||||
OnlyForSignedRequests = onlyForSignedRequests;
|
||||
IgnoreTotalRateLimit = ignoreTotalRateLimit;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SingleTopicRateLimiter: Limiter
|
||||
{
|
||||
public object Topic { get; set; }
|
||||
|
||||
public SingleTopicRateLimiter(object topic, Limiter limiter)
|
||||
:base(limiter.Type, limiter.Limit, limiter.Period, limiter.Method)
|
||||
{
|
||||
Topic = topic;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (Type == RateLimitType.ApiKey ? nameof(ApiKeyRateLimiter): nameof(EndpointRateLimiter)) + $": {Topic}";
|
||||
}
|
||||
}
|
||||
|
||||
internal enum RateLimitType
|
||||
{
|
||||
Total,
|
||||
Endpoint,
|
||||
PartialEndpoint,
|
||||
ApiKey
|
||||
}
|
||||
}
|
||||
}
|
89
CryptoExchange.Net/Objects/TimeSyncState.cs
Normal file
89
CryptoExchange.Net/Objects/TimeSyncState.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The time synchronization state of an API client
|
||||
/// </summary>
|
||||
public class TimeSyncState
|
||||
{
|
||||
/// <summary>
|
||||
/// Semaphore to use for checking the time syncing. Should be shared instance among the API client
|
||||
/// </summary>
|
||||
public SemaphoreSlim Semaphore { get; }
|
||||
/// <summary>
|
||||
/// Last sync time for the API client
|
||||
/// </summary>
|
||||
public DateTime LastSyncTime { get; set; }
|
||||
/// <summary>
|
||||
/// Time offset for the API client
|
||||
/// </summary>
|
||||
public TimeSpan TimeOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TimeSyncState()
|
||||
{
|
||||
Semaphore = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time synchronization info
|
||||
/// </summary>
|
||||
public class TimeSyncInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
public Log Log { get; }
|
||||
/// <summary>
|
||||
/// Should synchronize time
|
||||
/// </summary>
|
||||
public bool SyncTime { get; }
|
||||
/// <summary>
|
||||
/// Timestamp recalulcation interval
|
||||
/// </summary>
|
||||
public TimeSpan RecalculationInterval { get; }
|
||||
/// <summary>
|
||||
/// Time sync state for the API client
|
||||
/// </summary>
|
||||
public TimeSyncState TimeSyncState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="syncTime"></param>
|
||||
/// <param name="syncState"></param>
|
||||
public TimeSyncInfo(Log log, bool syncTime, TimeSyncState syncState)
|
||||
{
|
||||
Log = log;
|
||||
SyncTime = syncTime;
|
||||
TimeSyncState = syncState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the time offset
|
||||
/// </summary>
|
||||
/// <param name="offset"></param>
|
||||
public void UpdateTimeOffset(TimeSpan offset)
|
||||
{
|
||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
||||
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
||||
{
|
||||
Log.Write(LogLevel.Information, $"Time offset within limits, set offset to 0ms");
|
||||
TimeSyncState.TimeOffset = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Write(LogLevel.Information, $"Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
TimeSyncState.TimeOffset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,19 +10,19 @@ namespace CryptoExchange.Net.OrderBook
|
||||
public class ProcessBufferRangeSequenceEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// First update id
|
||||
/// First sequence number in this update
|
||||
/// </summary>
|
||||
public long FirstUpdateId { get; set; }
|
||||
/// <summary>
|
||||
/// Last update id
|
||||
/// Last sequence number in this update
|
||||
/// </summary>
|
||||
public long LastUpdateId { get; set; }
|
||||
/// <summary>
|
||||
/// List of asks
|
||||
/// List of changed/new asks
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||
/// <summary>
|
||||
/// List of bids
|
||||
/// List of changed/new bids
|
||||
/// </summary>
|
||||
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limiting object
|
||||
/// </summary>
|
||||
public class RateLimitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Lock
|
||||
/// </summary>
|
||||
public object LockObject { get; }
|
||||
private List<DateTime> Times { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RateLimitObject()
|
||||
{
|
||||
LockObject = new object();
|
||||
Times = new List<DateTime>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get time to wait for a specific time
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="perTimePeriod"></param>
|
||||
/// <returns></returns>
|
||||
public int GetWaitTime(DateTime time, int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
Times.RemoveAll(d => d < time - perTimePeriod);
|
||||
if (Times.Count >= limit)
|
||||
return (int)Math.Round((Times.First() - (time - perTimePeriod)).TotalMilliseconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an executed request time
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
public void Add(DateTime time)
|
||||
{
|
||||
Times.Add(time);
|
||||
Times.Sort();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits the amount of requests per time period to a certain limit, counts the request per API key.
|
||||
/// </summary>
|
||||
public class RateLimiterAPIKey: IRateLimiter, IDisposable
|
||||
{
|
||||
internal Dictionary<string, RateLimitObject> history = new Dictionary<string, RateLimitObject>();
|
||||
|
||||
private readonly SHA256 encryptor;
|
||||
private readonly int limitPerKey;
|
||||
private readonly TimeSpan perTimePeriod;
|
||||
private readonly object historyLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiterAPIKey. This rate limiter limits the amount of requests per time period to a certain limit, counts the request per API key.
|
||||
/// </summary>
|
||||
/// <param name="limitPerApiKey">The amount to limit to</param>
|
||||
/// <param name="perTimePeriod">The time period over which the limit counts</param>
|
||||
public RateLimiterAPIKey(int limitPerApiKey, TimeSpan perTimePeriod)
|
||||
{
|
||||
limitPerKey = limitPerApiKey;
|
||||
encryptor = SHA256.Create();
|
||||
this.perTimePeriod = perTimePeriod;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits = 1)
|
||||
{
|
||||
if(client.authProvider?.Credentials?.Key == null)
|
||||
return new CallResult<double>(0, null);
|
||||
|
||||
var keyBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(client.authProvider.Credentials.Key.GetString()));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < keyBytes.Length; i++)
|
||||
{
|
||||
builder.Append(keyBytes[i].ToString("x2"));
|
||||
}
|
||||
|
||||
var key = builder.ToString();
|
||||
|
||||
int waitTime;
|
||||
RateLimitObject rlo;
|
||||
lock (historyLock)
|
||||
{
|
||||
if (history.ContainsKey(key))
|
||||
rlo = history[key];
|
||||
else
|
||||
{
|
||||
rlo = new RateLimitObject();
|
||||
history.Add(key, rlo);
|
||||
}
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
lock (rlo.LockObject)
|
||||
{
|
||||
sw.Stop();
|
||||
waitTime = rlo.GetWaitTime(DateTime.UtcNow, limitPerKey, perTimePeriod);
|
||||
if (waitTime != 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
return new CallResult<double>(waitTime, new RateLimitError($"endpoint limit of {limitPerKey} reached on api key " + key));
|
||||
|
||||
Thread.Sleep(Convert.ToInt32(waitTime));
|
||||
waitTime += (int)sw.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
rlo.Add(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
return new CallResult<double>(waitTime, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
encryptor.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits the amount of requests per time period to a certain limit, counts the total amount of requests.
|
||||
/// </summary>
|
||||
public class RateLimiterCredit : IRateLimiter
|
||||
{
|
||||
internal List<DateTime> history = new List<DateTime>();
|
||||
|
||||
private readonly int limit;
|
||||
private readonly TimeSpan perTimePeriod;
|
||||
private readonly object requestLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiterTotal. This rate limiter limits the amount of requests per time period to a certain limit, counts the total amount of requests.
|
||||
/// </summary>
|
||||
/// <param name="limit">The amount to limit to</param>
|
||||
/// <param name="perTimePeriod">The time period over which the limit counts</param>
|
||||
public RateLimiterCredit(int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
this.limit = limit;
|
||||
this.perTimePeriod = perTimePeriod;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits = 1)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
lock (requestLock)
|
||||
{
|
||||
sw.Stop();
|
||||
double waitTime = 0;
|
||||
var checkTime = DateTime.UtcNow;
|
||||
history.RemoveAll(d => d < checkTime - perTimePeriod);
|
||||
|
||||
if (history.Count >= limit)
|
||||
{
|
||||
waitTime = (history.First() - (checkTime - perTimePeriod)).TotalMilliseconds;
|
||||
if (waitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
return new CallResult<double>(waitTime, new RateLimitError($"total limit of {limit} reached"));
|
||||
|
||||
Thread.Sleep(Convert.ToInt32(waitTime));
|
||||
waitTime += sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i <= credits; i++)
|
||||
history.Add(DateTime.UtcNow);
|
||||
|
||||
history.Sort();
|
||||
return new CallResult<double>(waitTime, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits the amount of requests per time period to a certain limit, counts the request per endpoint.
|
||||
/// </summary>
|
||||
public class RateLimiterPerEndpoint: IRateLimiter
|
||||
{
|
||||
internal Dictionary<string, RateLimitObject> history = new Dictionary<string, RateLimitObject>();
|
||||
|
||||
private readonly int limitPerEndpoint;
|
||||
private readonly TimeSpan perTimePeriod;
|
||||
private readonly object historyLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiterPerEndpoint. This rate limiter limits the amount of requests per time period to a certain limit, counts the request per endpoint.
|
||||
/// </summary>
|
||||
/// <param name="limitPerEndpoint">The amount to limit to</param>
|
||||
/// <param name="perTimePeriod">The time period over which the limit counts</param>
|
||||
public RateLimiterPerEndpoint(int limitPerEndpoint, TimeSpan perTimePeriod)
|
||||
{
|
||||
this.limitPerEndpoint = limitPerEndpoint;
|
||||
this.perTimePeriod = perTimePeriod;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitingBehaviour, int credits = 1)
|
||||
{
|
||||
int waitTime;
|
||||
RateLimitObject rlo;
|
||||
lock (historyLock)
|
||||
{
|
||||
if (history.ContainsKey(url))
|
||||
rlo = history[url];
|
||||
else
|
||||
{
|
||||
rlo = new RateLimitObject();
|
||||
history.Add(url, rlo);
|
||||
}
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
lock (rlo.LockObject)
|
||||
{
|
||||
sw.Stop();
|
||||
waitTime = rlo.GetWaitTime(DateTime.UtcNow, limitPerEndpoint, perTimePeriod);
|
||||
if (waitTime != 0)
|
||||
{
|
||||
if(limitingBehaviour == RateLimitingBehaviour.Fail)
|
||||
return new CallResult<double>(waitTime, new RateLimitError($"endpoint limit of {limitPerEndpoint} reached on endpoint " + url));
|
||||
|
||||
Thread.Sleep(Convert.ToInt32(waitTime));
|
||||
waitTime += (int)sw.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
rlo.Add(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
return new CallResult<double>(waitTime, null);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits the amount of requests per time period to a certain limit, counts the total amount of requests.
|
||||
/// </summary>
|
||||
public class RateLimiterTotal: IRateLimiter
|
||||
{
|
||||
internal List<DateTime> history = new List<DateTime>();
|
||||
|
||||
private readonly int limit;
|
||||
private readonly TimeSpan perTimePeriod;
|
||||
private readonly object requestLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiterTotal. This rate limiter limits the amount of requests per time period to a certain limit, counts the total amount of requests.
|
||||
/// </summary>
|
||||
/// <param name="limit">The amount to limit to</param>
|
||||
/// <param name="perTimePeriod">The time period over which the limit counts</param>
|
||||
public RateLimiterTotal(int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
this.limit = limit;
|
||||
this.perTimePeriod = perTimePeriod;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits = 1)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
lock (requestLock)
|
||||
{
|
||||
sw.Stop();
|
||||
double waitTime = 0;
|
||||
var checkTime = DateTime.UtcNow;
|
||||
history.RemoveAll(d => d < checkTime - perTimePeriod);
|
||||
|
||||
if (history.Count >= limit)
|
||||
{
|
||||
waitTime = (history.First() - (checkTime - perTimePeriod)).TotalMilliseconds;
|
||||
if (waitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
return new CallResult<double>(waitTime, new RateLimitError($"total limit of {limit} reached"));
|
||||
|
||||
Thread.Sleep(Convert.ToInt32(waitTime));
|
||||
waitTime += sw.ElapsedMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
history.Add(DateTime.UtcNow);
|
||||
history.Sort();
|
||||
return new CallResult<double>(waitTime, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ using CryptoExchange.Net.Interfaces;
|
||||
namespace CryptoExchange.Net.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Request object
|
||||
/// Request object, wrapper for HttpRequestMessage
|
||||
/// </summary>
|
||||
public class Request : IRequest
|
||||
{
|
||||
@ -49,6 +49,7 @@ namespace CryptoExchange.Net.Requests
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => request.RequestUri;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int RequestId { get; }
|
||||
|
||||
|
@ -7,7 +7,7 @@ using CryptoExchange.Net.Objects;
|
||||
namespace CryptoExchange.Net.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// WebRequest factory
|
||||
/// Request factory
|
||||
/// </summary>
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Requests
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequest Create(HttpMethod method, string uri, int requestId)
|
||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||
{
|
||||
if (httpClient == null)
|
||||
throw new InvalidOperationException("Cant create request before configuring http client");
|
||||
|
@ -8,7 +8,7 @@ using CryptoExchange.Net.Interfaces;
|
||||
namespace CryptoExchange.Net.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpWebResponse response object
|
||||
/// Response object, wrapper for HttpResponseMessage
|
||||
/// </summary>
|
||||
internal class Response : IResponse
|
||||
{
|
||||
|
@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
private DateTime _lastReceivedMessagesUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Received messages time -> size
|
||||
/// Received messages, the size and the timstamp
|
||||
/// </summary>
|
||||
protected readonly List<ReceiveItem> _receivedMessages;
|
||||
/// <summary>
|
||||
@ -72,17 +72,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
|
||||
|
||||
/// <summary>
|
||||
/// The id of this socket
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public int Id { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Origin { get; set; }
|
||||
/// <summary>
|
||||
/// Whether this socket is currently reconnecting
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Reconnecting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp this socket has been active for the last time
|
||||
/// </summary>
|
||||
@ -92,22 +90,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
public Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
public Func<string, string>? DataInterpreterString { get; set; }
|
||||
/// <summary>
|
||||
/// Url this socket connects to
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Url { get; }
|
||||
/// <summary>
|
||||
/// If the connection is closed
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsClosed => _socket.State == WebSocketState.Closed;
|
||||
|
||||
/// <summary>
|
||||
/// If the connection is open
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsOpen => _socket.State == WebSocketState.Open && !_closing;
|
||||
|
||||
/// <summary>
|
||||
@ -116,9 +111,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
public SslProtocols SSLProtocols { get; set; }
|
||||
|
||||
private Encoding _encoding = Encoding.UTF8;
|
||||
/// <summary>
|
||||
/// Encoding used for decoding the received bytes into a string
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public Encoding? Encoding
|
||||
{
|
||||
get => _encoding;
|
||||
@ -128,19 +121,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
_encoding = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of outgoing messages per second
|
||||
/// </summary>
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timespan no data is received on the socket. If no data is received within this time an error is generated
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public TimeSpan Timeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
get
|
||||
@ -152,38 +142,33 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!_receivedMessages.Any())
|
||||
return 0;
|
||||
|
||||
return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000 / 3d);
|
||||
return Math.Round(_receivedMessages.Sum(v => v.Bytes) / 1000d / 3d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Socket closed event
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public event Action OnClose
|
||||
{
|
||||
add => closeHandlers.Add(value);
|
||||
remove => closeHandlers.Remove(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// Socket message received event
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<string> OnMessage
|
||||
{
|
||||
add => messageHandlers.Add(value);
|
||||
remove => messageHandlers.Remove(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// Socket error event
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<Exception> OnError
|
||||
{
|
||||
add => errorHandlers.Add(value);
|
||||
remove => errorHandlers.Remove(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// Socket opened event
|
||||
/// </summary>
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action OnOpen
|
||||
{
|
||||
add => openHandlers.Add(value);
|
||||
@ -224,10 +209,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
_socket = CreateSocket();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a proxy to use. Should be set before connecting
|
||||
/// </summary>
|
||||
/// <param name="proxy"></param>
|
||||
/// <inheritdoc />
|
||||
public virtual void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
_socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port);
|
||||
@ -235,17 +217,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
_socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect the websocket
|
||||
/// </summary>
|
||||
/// <returns>True if successfull</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<bool> ConnectAsync()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} connecting");
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
await _socket.ConnectAsync(new Uri(Url), default).ConfigureAwait(false);
|
||||
await _socket.ConnectAsync(new Uri(Url), tcs.Token).ConfigureAwait(false);
|
||||
|
||||
Handle(openHandlers);
|
||||
}
|
||||
@ -270,10 +249,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send data over the websocket
|
||||
/// </summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <inheritdoc />
|
||||
public virtual void Send(string data)
|
||||
{
|
||||
if (_closing)
|
||||
@ -285,10 +261,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
_sendEvent.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the websocket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc />
|
||||
public virtual async Task CloseAsync()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} closing");
|
||||
@ -344,9 +317,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} disposed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the socket so a new connection can be attempted after it has been connected before
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Id} resetting");
|
||||
@ -400,8 +371,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
DateTime? start = null;
|
||||
while (MessagesSentLastSecond() >= RatelimitPerSecond)
|
||||
{
|
||||
if (start == null)
|
||||
start = DateTime.UtcNow;
|
||||
start ??= DateTime.UtcNow;
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -417,7 +387,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// cancelled
|
||||
// canceled
|
||||
break;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
@ -478,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// cancelled
|
||||
// canceled
|
||||
break;
|
||||
}
|
||||
catch (WebSocketException wse)
|
||||
@ -508,8 +478,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// We received data, but it is not complete, write it to a memory stream for reassembling
|
||||
multiPartMessage = true;
|
||||
if (memoryStream == null)
|
||||
memoryStream = new MemoryStream();
|
||||
memoryStream ??= new MemoryStream();
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
|
||||
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
|
||||
}
|
||||
@ -519,7 +488,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
// Received a complete message and it's not multi part
|
||||
log.Write(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
|
||||
HandleMessage(buffer.Array, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
||||
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -615,7 +584,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
catch(Exception e)
|
||||
{
|
||||
log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -645,7 +613,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// cancelled
|
||||
// canceled
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
public DateTime ReceivedTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="jsonData"></param>
|
||||
|
@ -14,7 +14,7 @@ using CryptoExchange.Net.Objects;
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket connecting
|
||||
/// A single socket connection to the server
|
||||
/// </summary>
|
||||
public class SocketConnection
|
||||
{
|
||||
@ -22,26 +22,32 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Connection lost event
|
||||
/// </summary>
|
||||
public event Action? ConnectionLost;
|
||||
|
||||
/// <summary>
|
||||
/// Connection closed and no reconnect is happening
|
||||
/// </summary>
|
||||
public event Action? ConnectionClosed;
|
||||
|
||||
/// <summary>
|
||||
/// Connecting restored event
|
||||
/// </summary>
|
||||
public event Action<TimeSpan>? ConnectionRestored;
|
||||
|
||||
/// <summary>
|
||||
/// The connection is paused event
|
||||
/// </summary>
|
||||
public event Action? ActivityPaused;
|
||||
|
||||
/// <summary>
|
||||
/// The connection is unpaused event
|
||||
/// </summary>
|
||||
public event Action? ActivityUnpaused;
|
||||
|
||||
/// <summary>
|
||||
/// Connecting closed event
|
||||
/// </summary>
|
||||
public event Action? Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Unhandled message event
|
||||
/// </summary>
|
||||
@ -57,35 +63,50 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If connection is authenticated
|
||||
/// If the connection has been authenticated
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If connection is made
|
||||
/// </summary>
|
||||
public bool Connected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The underlying socket
|
||||
/// The underlying websocket
|
||||
/// </summary>
|
||||
public IWebsocket Socket { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The API client the connection is for
|
||||
/// </summary>
|
||||
public SocketApiClient ApiClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the socket should be reconnected upon closing
|
||||
/// </summary>
|
||||
public bool ShouldReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current reconnect try
|
||||
/// Current reconnect try, reset when a successful connection is made
|
||||
/// </summary>
|
||||
public int ReconnectTry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current resubscribe try
|
||||
/// Current resubscribe try, reset when a successful connection is made
|
||||
/// </summary>
|
||||
public int ResubscribeTry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time of disconnecting
|
||||
/// </summary>
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tag for identificaion
|
||||
/// </summary>
|
||||
public string? Tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If activity is paused
|
||||
/// </summary>
|
||||
@ -110,7 +131,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
private bool lostTriggered;
|
||||
private readonly Log log;
|
||||
private readonly SocketClient socketClient;
|
||||
private readonly BaseSocketClient socketClient;
|
||||
|
||||
private readonly List<PendingRequest> pendingRequests;
|
||||
|
||||
@ -118,18 +139,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// New socket connection
|
||||
/// </summary>
|
||||
/// <param name="client">The socket client</param>
|
||||
/// <param name="apiClient">The api client</param>
|
||||
/// <param name="socket">The socket</param>
|
||||
public SocketConnection(SocketClient client, IWebsocket socket)
|
||||
public SocketConnection(BaseSocketClient client, SocketApiClient apiClient, IWebsocket socket)
|
||||
{
|
||||
log = client.log;
|
||||
socketClient = client;
|
||||
ApiClient = apiClient;
|
||||
|
||||
pendingRequests = new List<PendingRequest>();
|
||||
|
||||
subscriptions = new List<SocketSubscription>();
|
||||
Socket = socket;
|
||||
|
||||
Socket.Timeout = client.SocketNoDataTimeout;
|
||||
Socket.Timeout = client.ClientOptions.SocketNoDataTimeout;
|
||||
Socket.OnMessage += ProcessMessage;
|
||||
Socket.OnClose += SocketOnClose;
|
||||
Socket.OnOpen += SocketOnOpen;
|
||||
@ -138,7 +161,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Process a message received by the socket
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="data">The received data</param>
|
||||
private void ProcessMessage(string data)
|
||||
{
|
||||
var timestamp = DateTime.UtcNow;
|
||||
@ -183,7 +206,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
// Message was not a request response, check data handlers
|
||||
var messageEvent = new MessageEvent(this, tokenData, socketClient.OutputOriginalData ? data: null, timestamp);
|
||||
var messageEvent = new MessageEvent(this, tokenData, socketClient.ClientOptions.OutputOriginalData ? data: null, timestamp);
|
||||
if (!HandleData(messageEvent) && !handledResponse)
|
||||
{
|
||||
if (!socketClient.UnhandledMessageExpected)
|
||||
@ -193,7 +216,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add subscription to this connection
|
||||
/// Add a subscription to this connection
|
||||
/// </summary>
|
||||
/// <param name="subscription"></param>
|
||||
public void AddSubscription(SocketSubscription subscription)
|
||||
@ -203,15 +226,31 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a subscription on this connection
|
||||
/// Get a subscription on this connection by id
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
public SocketSubscription GetSubscription(int id)
|
||||
public SocketSubscription? GetSubscription(int id)
|
||||
{
|
||||
lock (subscriptionLock)
|
||||
return subscriptions.SingleOrDefault(s => s.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a subscription on this connection by its subscribe request
|
||||
/// </summary>
|
||||
/// <param name="predicate">Filter for a request</param>
|
||||
/// <returns></returns>
|
||||
public SocketSubscription? GetSubscriptionByRequest(Func<object?, bool> predicate)
|
||||
{
|
||||
lock(subscriptionLock)
|
||||
return subscriptions.SingleOrDefault(s => predicate(s.Request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process data
|
||||
/// </summary>
|
||||
/// <param name="messageEvent"></param>
|
||||
/// <returns>True if the data was successfully handled</returns>
|
||||
private bool HandleData(MessageEvent messageEvent)
|
||||
{
|
||||
SocketSubscription? currentSubscription = null;
|
||||
@ -230,7 +269,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
currentSubscription = subscription;
|
||||
if (subscription.Request == null)
|
||||
{
|
||||
if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Identifier!))
|
||||
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Identifier!))
|
||||
{
|
||||
handled = true;
|
||||
subscription.MessageHandler(messageEvent);
|
||||
@ -238,7 +277,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
else
|
||||
{
|
||||
if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Request))
|
||||
if (socketClient.MessageMatchesHandler(this, messageEvent.JsonData, subscription.Request))
|
||||
{
|
||||
handled = true;
|
||||
messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData);
|
||||
@ -249,7 +288,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
sw.Stop();
|
||||
if (sw.ElapsedMilliseconds > 500)
|
||||
log.Write(LogLevel.Warning, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " +
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " +
|
||||
"Data from this socket may arrive late or not at all if message processing is continuously slow.");
|
||||
else
|
||||
log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms");
|
||||
@ -269,7 +308,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <typeparam name="T">The data type expected in response</typeparam>
|
||||
/// <param name="obj">The object to send</param>
|
||||
/// <param name="timeout">The timeout for response</param>
|
||||
/// <param name="handler">The response handler</param>
|
||||
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
|
||||
/// <returns></returns>
|
||||
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
|
||||
{
|
||||
@ -330,7 +369,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
}
|
||||
|
||||
if (socketClient.AutoReconnect && ShouldReconnect)
|
||||
if (socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
||||
{
|
||||
if (Socket.Reconnecting)
|
||||
return; // Already reconnecting
|
||||
@ -338,7 +377,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
Socket.Reconnecting = true;
|
||||
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}");
|
||||
log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect{(ReconnectTry == 0 ? "": $" after {socketClient.ClientOptions.ReconnectInterval}")}");
|
||||
if (!lostTriggered)
|
||||
{
|
||||
lostTriggered = true;
|
||||
@ -349,8 +388,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
while (ShouldReconnect)
|
||||
{
|
||||
// Wait a bit before attempting reconnect
|
||||
await Task.Delay(socketClient.ReconnectInterval).ConfigureAwait(false);
|
||||
if (ReconnectTry > 0)
|
||||
{
|
||||
// Wait a bit before attempting reconnect
|
||||
await Task.Delay(socketClient.ClientOptions.ReconnectInterval).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!ShouldReconnect)
|
||||
{
|
||||
// Should reconnect changed to false while waiting to reconnect
|
||||
@ -363,8 +406,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
ReconnectTry++;
|
||||
ResubscribeTry = 0;
|
||||
if (socketClient.MaxReconnectTries != null
|
||||
&& ReconnectTry >= socketClient.MaxReconnectTries)
|
||||
if (socketClient.ClientOptions.MaxReconnectTries != null
|
||||
&& ReconnectTry >= socketClient.ClientOptions.MaxReconnectTries)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect after {ReconnectTry} tries, closing");
|
||||
ShouldReconnect = false;
|
||||
@ -377,7 +420,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
break;
|
||||
}
|
||||
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect{(socketClient.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.MaxReconnectTries}": "")}");
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect{(socketClient.ClientOptions.MaxReconnectTries != null ? $", try {ReconnectTry}/{socketClient.ClientOptions.MaxReconnectTries}": "")}, will try again in {socketClient.ClientOptions.ReconnectInterval}");
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -391,9 +434,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!reconnectResult)
|
||||
{
|
||||
ResubscribeTry++;
|
||||
DisconnectTime = time;
|
||||
|
||||
if (socketClient.MaxResubscribeTries != null &&
|
||||
ResubscribeTry >= socketClient.MaxResubscribeTries)
|
||||
if (socketClient.ClientOptions.MaxResubscribeTries != null &&
|
||||
ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to resubscribe after {ResubscribeTry} tries, closing");
|
||||
ShouldReconnect = false;
|
||||
@ -405,7 +449,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||
}
|
||||
else
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket{(socketClient.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.MaxResubscribeTries}" : "")}. Disconnecting and reconnecting.");
|
||||
log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket{(socketClient.ClientOptions.MaxResubscribeTries != null ? $", try {ResubscribeTry}/{socketClient.ClientOptions.MaxResubscribeTries}" : "")}. Disconnecting and reconnecting.");
|
||||
|
||||
if (Socket.IsOpen)
|
||||
await Socket.CloseAsync().ConfigureAwait(false);
|
||||
@ -419,7 +463,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (lostTriggered)
|
||||
{
|
||||
lostTriggered = false;
|
||||
InvokeConnectionRestored(time);
|
||||
_ = Task.Run(() => ConnectionRestored?.Invoke(time.HasValue ? DateTime.UtcNow - time.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -431,7 +475,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!socketClient.AutoReconnect && ShouldReconnect)
|
||||
if (!socketClient.ClientOptions.AutoReconnect && ShouldReconnect)
|
||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||
|
||||
// No reconnecting needed
|
||||
@ -443,11 +487,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
}
|
||||
|
||||
private async void InvokeConnectionRestored(DateTime? disconnectTime)
|
||||
{
|
||||
await Task.Run(() => ConnectionRestored?.Invoke(disconnectTime.HasValue ? DateTime.UtcNow - disconnectTime.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessReconnectAsync()
|
||||
{
|
||||
if (Authenticated)
|
||||
@ -472,11 +511,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
subscriptionList = subscriptions.Where(h => h.Request != null).ToList();
|
||||
|
||||
// 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.MaxConcurrentResubscriptionsPerSocket)
|
||||
for (var i = 0; i < subscriptionList.Count; i += socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
||||
{
|
||||
var success = true;
|
||||
var taskList = new List<Task>();
|
||||
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.MaxConcurrentResubscriptionsPerSocket))
|
||||
foreach (var subscription in subscriptionList.Skip(i).Take(socketClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
||||
{
|
||||
if (!Socket.IsOpen)
|
||||
continue;
|
||||
@ -506,7 +545,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
internal async Task<CallResult<bool>> ResubscribeAsync(SocketSubscription socketSubscription)
|
||||
{
|
||||
if (!Socket.IsOpen)
|
||||
return new CallResult<bool>(false, 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);
|
||||
}
|
||||
@ -521,7 +560,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
ShouldReconnect = false;
|
||||
if (socketClient.sockets.ContainsKey(Socket.Id))
|
||||
socketClient.sockets.TryRemove(Socket.Id, out _);
|
||||
|
||||
|
||||
lock (subscriptionLock)
|
||||
{
|
||||
foreach (var subscription in subscriptions)
|
||||
{
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
}
|
||||
}
|
||||
await Socket.CloseAsync().ConfigureAwait(false);
|
||||
Socket.Dispose();
|
||||
}
|
||||
@ -536,10 +583,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!Socket.IsOpen)
|
||||
return;
|
||||
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
|
||||
if (subscription.Confirmed)
|
||||
await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false);
|
||||
|
||||
var shouldCloseConnection = false;
|
||||
bool shouldCloseConnection;
|
||||
lock (subscriptionLock)
|
||||
shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
@ -8,9 +9,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
public class SocketSubscription
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscription id
|
||||
/// Unique subscription id
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exception event
|
||||
/// </summary>
|
||||
@ -22,23 +24,31 @@ namespace CryptoExchange.Net.Sockets
|
||||
public Action<MessageEvent> MessageHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Request object
|
||||
/// The request object send when subscribing on the server. Either this or the `Identifier` property should be set
|
||||
/// </summary>
|
||||
public object? Request { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Subscription identifier
|
||||
/// The subscription identifier, used instead of a `Request` object to identify the subscription
|
||||
/// </summary>
|
||||
public string? Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is user subscription or generic
|
||||
/// Whether this is a user subscription or an internal listener
|
||||
/// </summary>
|
||||
public bool UserSubscription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the subscription has been confirmed
|
||||
/// If the subscription has been confirmed to be subscribed by the server
|
||||
/// </summary>
|
||||
public bool Confirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
|
||||
/// a provided cancelation token
|
||||
/// </summary>
|
||||
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
|
||||
|
||||
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, Action<MessageEvent> dataHandler)
|
||||
{
|
||||
Id = id;
|
||||
@ -49,7 +59,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create SocketSubscription for a request
|
||||
/// Create SocketSubscription for a subscribe request
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="request"></param>
|
||||
|
@ -22,8 +22,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the <see cref="SocketClientOptions.MaxReconnectTries"/> and <see cref="SocketClientOptions.MaxResubscribeTries"/> options,
|
||||
/// or <see cref="SocketClientOptions.AutoReconnect"/> is false
|
||||
/// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the <see cref="BaseSocketClientOptions.MaxReconnectTries"/> and <see cref="BaseSocketClientOptions.MaxResubscribeTries"/> options,
|
||||
/// or <see cref="BaseSocketClientOptions.AutoReconnect"/> is false. The socket will not be reconnected
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
@ -33,8 +33,8 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting.
|
||||
/// Note that when the executing code is suspended and resumed at a later period (for example laptop going to sleep) the disconnect time will be incorrect as the diconnect
|
||||
/// will only be detected after resuming. This will lead to an incorrect disconnected timespan.
|
||||
/// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the diconnect
|
||||
/// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes.
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored
|
||||
{
|
||||
|
10
Examples/BlazorClient/App.razor
Normal file
10
Examples/BlazorClient/App.razor
Normal file
@ -0,0 +1,10 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
24
Examples/BlazorClient/BlazorClient.csproj
Normal file
24
Examples/BlazorClient/BlazorClient.csproj
Normal file
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="8.0.0-beta1" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="5.0.0-beta1" />
|
||||
<PackageReference Include="Bittrex.Net" Version="7.0.0-beta1" />
|
||||
<PackageReference Include="Bybit.Net" Version="0.0.1-beta2" />
|
||||
<PackageReference Include="CoinEx.Net" Version="5.0.0-beta1" />
|
||||
<PackageReference Include="FTX.Net" Version="1.0.0-beta1" />
|
||||
<PackageReference Include="Huobi.Net" Version="4.0.0-beta1" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="3.0.0-beta1" />
|
||||
<PackageReference Include="Kucoin.Net" Version="4.0.0-beta3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00250" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
63
Examples/BlazorClient/Pages/Index.razor
Normal file
63
Examples/BlazorClient/Pages/Index.razor
Normal file
@ -0,0 +1,63 @@
|
||||
@page "/"
|
||||
@inject IBinanceClient binanceClient
|
||||
@inject IBitfinexClient bitfinexClient
|
||||
@inject IBittrexClient bittrexClient
|
||||
@inject IBybitClient bybitClient
|
||||
@inject ICoinExClient coinexClient
|
||||
@inject IFTXClient ftxClient
|
||||
@inject IHuobiClient huobiClient
|
||||
@inject IKrakenClient krakenClient
|
||||
@inject IKucoinClient kucoinClient
|
||||
|
||||
<h3>BTC-USD prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
{
|
||||
<div>@price.Key: @price.Value</div>
|
||||
}
|
||||
|
||||
@code{
|
||||
private Dictionary<string, decimal> _prices = new Dictionary<string, decimal>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var bybitTask = bybitClient.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 krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
|
||||
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, ftxTask, huobiTask, krakenTask, kucoinTask);
|
||||
|
||||
if (binanceTask.Result.Success)
|
||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||
|
||||
if (bitfinexTask.Result.Success)
|
||||
_prices.Add("Bitfinex", bitfinexTask.Result.Data.LastPrice);
|
||||
|
||||
if (bittrexTask.Result.Success)
|
||||
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
||||
|
||||
if (bybitTask.Result.Success)
|
||||
_prices.Add("Bybit", bybitTask.Result.Data.LastPrice);
|
||||
|
||||
if (coinexTask.Result.Success)
|
||||
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
|
||||
|
||||
if (ftxTask.Result.Success)
|
||||
_prices.Add("FTX", ftxTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (huobiTask.Result.Success)
|
||||
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
|
||||
|
||||
if (krakenTask.Result.Success)
|
||||
_prices.Add("Kraken", krakenTask.Result.Data.First().Value.LastTrade.Price);
|
||||
|
||||
if (kucoinTask.Result.Success)
|
||||
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
|
||||
}
|
||||
|
||||
}
|
65
Examples/BlazorClient/Pages/LiveData.razor
Normal file
65
Examples/BlazorClient/Pages/LiveData.razor
Normal file
@ -0,0 +1,65 @@
|
||||
@page "/LiveData"
|
||||
@inject IBinanceSocketClient binanceSocketClient
|
||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||
@inject IBittrexSocketClient bittrexSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@inject IFTXSocketClient ftxSocketClient
|
||||
@inject IHuobiSocketClient huobiSocketClient
|
||||
@inject IKrakenSocketClient krakenSocketClient
|
||||
@inject IKucoinSocketClient kucoinSocketClient
|
||||
@using Binance.Net.Clients.SpotApi
|
||||
@using Bitfinex.Net.Clients.SpotApi
|
||||
@using Bittrex.Net.Clients.SpotApi
|
||||
@using Bybit.Net.Clients.SpotApi
|
||||
@using CoinEx.Net.Clients.SpotApi
|
||||
@using CryptoExchange.Net.Objects
|
||||
@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
|
||||
|
||||
<h3>ETH-BTC prices, live updates:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
{
|
||||
<div>@price.Key: @price.Value</div>
|
||||
}
|
||||
|
||||
@code{
|
||||
private ConcurrentDictionary<string, decimal> _prices = new ConcurrentDictionary<string, decimal>();
|
||||
private UpdateSubscription[] _subs = Array.Empty<UpdateSubscription>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
||||
{
|
||||
binanceSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bittrexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
||||
bybitSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||
ftxSocketClient.Streams.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("FTX", data.Data.LastPrice ?? 0)),
|
||||
huobiSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||
krakenSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||
kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
_subs = tasks.Where(t => t.Result.Success).Select(t => t.Result.Data).ToArray();
|
||||
}
|
||||
|
||||
private void UpdateData(string exchange, decimal price)
|
||||
{
|
||||
_prices[exchange] = price;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var sub in _subs)
|
||||
// It's not necessary to wait for this
|
||||
_ = sub.CloseAsync();
|
||||
}
|
||||
}
|
77
Examples/BlazorClient/Pages/OrderBooks.razor
Normal file
77
Examples/BlazorClient/Pages/OrderBooks.razor
Normal file
@ -0,0 +1,77 @@
|
||||
@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.Timers
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC books, live updates:</h3>
|
||||
<div style="display:flex; flex-wrap: wrap;">
|
||||
@foreach(var book in _books.OrderBy(p => p.Key))
|
||||
{
|
||||
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
|
||||
<h4>@book.Key</h4>
|
||||
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
<div>@book.Value.Bids.ElementAt(i).Price - @book.Value.Asks.ElementAt(i).Price</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code{
|
||||
private Dictionary<string, ISymbolOrderBook> _books = new Dictionary<string, ISymbolOrderBook>();
|
||||
private Timer _timer;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
|
||||
KucoinClient.SetDefaultOptions(new Kucoin.Net.Objects.KucoinClientOptions
|
||||
{
|
||||
ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE")
|
||||
});
|
||||
|
||||
_books = new Dictionary<string, ISymbolOrderBook>
|
||||
{
|
||||
{ "Binance", new BinanceSpotSymbolOrderBook("ETHBTC") },
|
||||
{ "Bitfinex", new BitfinexSymbolOrderBook("tETHBTC") },
|
||||
{ "Bittrex", new BittrexSymbolOrderBook("ETH-BTC") },
|
||||
{ "Bybit", new BybitSpotSymbolOrderBook("ETHBTC") },
|
||||
{ "CoinEx", new CoinExSpotSymbolOrderBook("ETHBTC") },
|
||||
{ "FTX", new FTXSymbolOrderBook("ETH/BTC") },
|
||||
{ "Huobi", new HuobiSpotSymbolOrderBook("ethbtc") },
|
||||
{ "Kraken", new KrakenSpotSymbolOrderBook("ETH/XBT") },
|
||||
{ "Kucoin", new KucoinSpotSymbolOrderBook("ETH-BTC") },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
|
||||
// Use a manual update timer so the page isn't refreshed too often
|
||||
_timer = new Timer(500);
|
||||
_timer.Start();
|
||||
_timer.Elapsed += (o, e) => InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
foreach (var book in _books)
|
||||
// It's not necessary to wait for this
|
||||
_ = book.Value.StopAsync();
|
||||
}
|
||||
}
|
56
Examples/BlazorClient/Pages/SpotClient.razor
Normal file
56
Examples/BlazorClient/Pages/SpotClient.razor
Normal file
@ -0,0 +1,56 @@
|
||||
@page "/SpotClient"
|
||||
@inject IBinanceClient binanceClient
|
||||
@inject IBitfinexClient bitfinexClient
|
||||
@inject IBittrexClient bittrexClient
|
||||
@inject IBybitClient bybitClient
|
||||
@inject ICoinExClient coinexClient
|
||||
@inject IFTXClient ftxClient
|
||||
@inject IHuobiClient huobiClient
|
||||
@inject IKrakenClient krakenClient
|
||||
@inject IKucoinClient kucoinClient
|
||||
@using Binance.Net.Clients.SpotApi
|
||||
@using Bitfinex.Net.Clients.SpotApi
|
||||
@using Bittrex.Net.Clients.SpotApi
|
||||
@using Bybit.Net.Clients.SpotApi
|
||||
@using CoinEx.Net.Clients.SpotApi
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using FTX.Net.Clients.TradeApi
|
||||
@using Huobi.Net.Clients.SpotApi
|
||||
@using Kraken.Net.Clients.SpotApi
|
||||
@using Kucoin.Net.Clients.SpotApi
|
||||
|
||||
<h3>ETH-BTC prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
{
|
||||
<div>@price.Key: @price.Value</div>
|
||||
}
|
||||
|
||||
@code{
|
||||
private Dictionary<string, decimal?> _prices = new Dictionary<string, decimal?>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var clients = new ISpotClient[]
|
||||
{
|
||||
|
||||
binanceClient.SpotApi.ComonSpotClient,
|
||||
bitfinexClient.SpotApi.ComonSpotClient,
|
||||
bittrexClient.SpotApi.ComonSpotClient,
|
||||
bybitClient.SpotApi.CommonSpotClient,
|
||||
coinexClient.SpotApi.ComonSpotClient,
|
||||
ftxClient.TradeApi.ComonSpotClient,
|
||||
huobiClient.SpotApi.ComonSpotClient,
|
||||
krakenClient.SpotApi.ComonSpotClient,
|
||||
kucoinClient.SpotApi.CommonSpotClient
|
||||
};
|
||||
|
||||
var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC"))));
|
||||
await Task.WhenAll(tasks.Select(t => t.Item2));
|
||||
foreach(var task in tasks)
|
||||
{
|
||||
if(task.Item2.Result.Success)
|
||||
_prices.Add(task.Item1, task.Item2.Result.Data.HighPrice);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
35
Examples/BlazorClient/Pages/_Host.cshtml
Normal file
35
Examples/BlazorClient/Pages/_Host.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
@page "/"
|
||||
@namespace BlazorClient.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlazorClient</title>
|
||||
<base href="~/" />
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link href="css/site.css" rel="stylesheet" />
|
||||
<link href="BlazorClient.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<component type="typeof(App)" render-mode="Server" />
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
<environment include="Staging,Production">
|
||||
An error has occurred. This application may no longer respond until reloaded.
|
||||
</environment>
|
||||
<environment include="Development">
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
</environment>
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
</body>
|
||||
</html>
|
31
Examples/BlazorClient/Program.cs
Normal file
31
Examples/BlazorClient/Program.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BlazorClient
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Error)
|
||||
.WriteTo.File("log-.txt", rollingInterval: RollingInterval.Day, flushToDiskInterval: System.TimeSpan.FromSeconds(1))
|
||||
.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()) ));
|
||||
});
|
||||
}
|
||||
}
|
13
Examples/BlazorClient/Shared/MainLayout.razor
Normal file
13
Examples/BlazorClient/Shared/MainLayout.razor
Normal file
@ -0,0 +1,13 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="content px-4">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user