1
0
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:
Jkorf 2022-02-18 11:06:34 +01:00
parent 8207a8f32a
commit c22b54c898
140 changed files with 7561 additions and 7182 deletions

View File

@ -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);
}
}
}

View 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
{
}
}

View 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
}
}

View File

@ -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>

View 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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();
}

View File

@ -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)

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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

View File

@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{
}

View File

@ -1,11 +0,0 @@
using System;
namespace CryptoExchange.Net.Attributes
{
/// <summary>
/// Marks property as optional
/// </summary>
public class JsonOptionalPropertyAttribute : Attribute
{
}
}

View 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;
}
}
}

View File

@ -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());

View File

@ -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);
}
}
}

View 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
}
}

View File

@ -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");
}
}
}

View 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();
}
}
}

View 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();
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View 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);
}
}
}

View 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)
{
}
}
}

View 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; }
}
}

View 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!;
}
}

View 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
}
}

View 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; }
}
}

View 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; }
}
}

View 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>();
}
}

View 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; }
}
}

View 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;
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -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>

View File

@ -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));

View 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));
}
}
}

View 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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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}"));
}
}
}
}

View File

@ -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?);
}
}
}

View File

@ -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

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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());
}
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View 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
}
}
}

View 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;
}
}
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }

View File

@ -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");

View File

@ -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
{

View File

@ -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;
}
}

View File

@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Sockets
public DateTime ReceivedTimestamp { get; set; }
/// <summary>
///
/// ctor
/// </summary>
/// <param name="connection"></param>
/// <param name="jsonData"></param>

View File

@ -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);

View File

@ -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>

View File

@ -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
{

View 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>

View 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>

View 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);
}
}

View 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();
}
}

View 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();
}
}

View 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);
}
}
}

View 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>

View 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()) ));
});
}
}

View 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