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

Compare commits

...

56 Commits

Author SHA1 Message Date
Jkorf a832f0e4d4 Updated to version 9.8.0 2025-09-30 12:06:14 +02:00
Jkorf 6ab4d005a0 Updated CryptoExchange.Net version to 9.8.0 2025-09-30 12:04:28 +02:00
Jkorf 18dc935038 Updated to version 9.8.0 2025-09-30 11:56:00 +02:00
Jkorf bb3a534f75 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2025-09-30 11:50:16 +02:00
nils2525 649ba370c6 Fixed EnumConverter to allow mapping empty string values (#253) 2025-09-30 11:49:58 +02:00
Jkorf 51732c5ce6 Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect 2025-09-29 14:43:39 +02:00
Jkorf 0ba7b46680 Added ContractAddress to SharedAsset model 2025-09-29 13:51:59 +02:00
Jkorf 94dfbb7b9e Fixed ExchangeHelpers.AdjustValueStep high precision calculation 2025-09-29 13:51:46 +02:00
Jkorf b8b7512b35 Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed 2025-09-29 10:07:28 +02:00
Jkorf aba6b773ce Added ITrackerFactory interface 2025-09-17 10:55:48 +02:00
Jkorf d88fb0d356 Added BloFin to ReadMe and examples 2025-09-17 10:01:17 +02:00
Jkorf b8c6d55156 CryptoManager.Net reference 2025-09-02 11:43:44 +02:00
Jkorf d9a5481db2 Updated to version 9.7.0 2025-09-01 13:37:16 +02:00
Jkorf 6a8bb42c0e Updated CryptoExchange.Net for CryptoExchange.Net.Protobuf version to 9.7.0 2025-09-01 13:35:24 +02:00
Jkorf 2445f001ab Updated to version 9.7.0 2025-09-01 13:18:16 +02:00
Jkorf c84fa9ac32 Fixed test 2025-09-01 13:16:20 +02:00
Jkorf d44a11c44e HttpVersion update
Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
Added REST client option for selecting HTTP protocol version
Added REST client option for HTTP client keep alive interval
Added HttpVersion to WebCallResult responses
Updated request logic to default to using HTTP version 2.0 for dotnet core
2025-09-01 10:12:59 +02:00
Jkorf b215cccda4 Updated to version 9.6.0 2025-08-25 10:17:35 +02:00
Jkorf 3eda488361 Updated CryptoExchange.Net.Protobuf to CryptoExchange.Net version 9.5.0 2025-08-25 10:15:14 +02:00
Jkorf 993a44de35 Updated to version 9.6.0 2025-08-25 10:03:17 +02:00
Jkorf 99465f99a1 Fixed test 2025-08-25 10:00:42 +02:00
Jkorf d42de1fe90 Added support for parsing REST response even though status indicates error 2025-08-25 09:58:03 +02:00
Jkorf d0284c62c0 Removed obsolete attribute on Error.Code property, updated the description 2025-08-22 16:12:01 +02:00
Jkorf d92f3b7904 Added better support for subscriptions without subscribe confirmation 2025-08-22 10:17:16 +02:00
Jkorf 3e1b5ada69 Added check in websocket for receiving 401 unauthorized http response status when 101 was expected 2025-08-21 13:39:07 +02:00
Jkorf 6156fb8154 Fixed test 2025-08-19 14:42:03 +02:00
Jkorf f2753aed1e Updated to version 9.5.0 2025-08-19 10:21:24 +02:00
Jkorf e33d826381 Updated CryptoExchange.Net version to 9.5.0 2025-08-19 10:20:11 +02:00
Jkorf 364aa4d324 Updated to version 9.5.0 2025-08-19 10:07:07 +02:00
Jkorf 455b332757 Updated test methods 2025-08-19 10:05:14 +02:00
Jkorf 876b895645 Fixed warning 2025-08-19 10:01:49 +02:00
Jkorf daf7ed9fe6 Refactored RestApiClient authentication to prevent duplicate query string / body serialization 2025-08-19 09:50:10 +02:00
Jan Korf 3e365f83c9 Error handling update 2025-08-18 11:03:26 +02:00
Jkorf 40977ebdbe Fixed timing issue in query response processing 2025-08-07 14:15:18 +02:00
Jkorf 4a9058fc1c Fix response type in websocket queries not interested in the response 2025-08-07 14:15:00 +02:00
JKorf dab9a21608 Fixed IOrderBookSocketClient Shared interface not getting registered in DI 2025-08-04 17:12:34 +02:00
Jkorf a89c222399 Updated to version 9.4.0 2025-08-04 09:56:45 +02:00
Jkorf 1e356d2a45 Updated CryptoExchange.Net and protobuf-net package versions for CryptoExchange.Net.Protobuf 2025-08-04 09:54:15 +02:00
Jkorf eed794c2cf Updated to version 9.4.0 2025-08-04 09:47:36 +02:00
Jkorf 2f82e2015b Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported 2025-08-04 09:39:30 +02:00
Jkorf ad599badb2 Added CryptoExchange.Net tag 2025-07-29 16:02:38 +02:00
Jkorf 1e45c73f1d Added CoinW to examples 2025-07-29 15:57:02 +02:00
Jkorf 49c1fda2c1 Added CoinW reference 2025-07-29 11:44:22 +02:00
Jkorf 32a31e464b Updated to version 9.3.1 2025-07-29 09:42:53 +02:00
Jkorf cddb4167e4 Added Id property to SharedPosition model 2025-07-28 15:58:36 +02:00
Jkorf 65457d8df2 Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum 2025-07-28 15:58:19 +02:00
Jkorf 122a6cad43 Updated to version 9.3.0 2025-07-23 10:52:40 +02:00
Jkorf 4c0e841425 Updated CryptoExchange.Net version 2025-07-23 10:51:17 +02:00
Jkorf 92f5839aec Updated to version 9.3.0 2025-07-23 10:37:52 +02:00
Jan Korf 30475dae67 Feature/websocket listener update (#244)
Updated websocket message to listener matching logic to be more flexible
2025-07-23 10:31:03 +02:00
Jkorf 3d942bd503 Updated decimal parser to support "NaN" and "-Infinity" strings, added check for negative overflow value, improved performance in most cases 2025-07-21 09:42:52 +02:00
Jkorf f739520e52 Updated to version 9.2.1 2025-07-16 10:51:41 +02:00
Jkorf 0152603ddb Added setting for whether or not to process unparsable websocket messages 2025-07-16 10:42:38 +02:00
Jkorf aa06e0eead Fixed issue causing duplicate subscriptions and data in the TradeTracker and KlineTracker when websocket connection was reconnected 2025-07-16 09:27:42 +02:00
Jkorf 2fde9a285e Update README.md 2025-07-14 13:31:47 +02:00
Jkorf b9f6eb6abb Update README.md 2025-07-14 13:25:01 +02:00
94 changed files with 2047 additions and 749 deletions
@@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch (Exception ex) catch (Exception ex)
{ {
return new CallResult<object>(new DeserializeError(ex.Message)); return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -320,7 +320,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch(Exception ex) catch(Exception ex)
{ {
return new CallResult<T>(new DeserializeError(ex.ToLogString())); return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -354,7 +354,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
{ {
// Not a json message // Not a json message
IsValid = false; IsValid = false;
return Task.FromResult(new CallResult(new DeserializeError("ProtoBufError: " + ex.Message, ex))); return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
} }
} }
@@ -446,7 +446,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch (Exception ex) catch (Exception ex)
{ {
return new CallResult<object>(new DeserializeError(ex.ToLogString())); return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -485,7 +485,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
} }
catch (Exception ex) catch (Exception ex)
{ {
return new CallResult<T>(new DeserializeError(ex.Message)); return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -504,7 +504,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
{ {
// Not a json message // Not a json message
IsValid = false; IsValid = false;
return new CallResult(new DeserializeError("ProtobufError: " + ex.Message, ex)); return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
} }
} }
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net.Protobuf</PackageId> <PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description> <Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.2.0</PackageVersion> <PackageVersion>9.8.0</PackageVersion>
<AssemblyVersion>9.2.0</AssemblyVersion> <AssemblyVersion>9.8.0</AssemblyVersion>
<FileVersion>9.2.0</FileVersion> <FileVersion>9.8.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags> <PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile> <DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.2.0" /> <PackageReference Include="CryptoExchange.Net" Version="9.8.0" />
<PackageReference Include="protobuf-net" Version="3.2.52" /> <PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+20 -1
View File
@@ -1,9 +1,28 @@
# ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Proto # ![.CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Proto
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net.Protobuf/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.NetProtobuf/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.NetProtobuf.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.NetProtobuf) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge) [![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.Protobuf.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
Protobuf support for CryptoExchange.Net. Protobuf support for CryptoExchange.Net.
## Release notes ## Release notes
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
* Version 9.5.0 - 19 Aug 2025
* Updated CryptoExchange.Net version to 9.5.0
* Version 9.4.0 - 04 Aug 2025
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Updated protobuf-net package version to 3.2.56
* Version 9.3.0 - 23 Jul 2025
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.2.0 - 14 Jul 2025 * Version 9.2.0 - 14 Jul 2025
* Initial release * Initial release
+18 -14
View File
@@ -1,9 +1,11 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -16,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestBasicErrorCallResult() public void TestBasicErrorCallResult()
{ {
var result = new CallResult(new ServerError("TestError")); var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.Message, "TestError"); ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
} }
@@ -36,9 +38,9 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestCallResultError() public void TestCallResultError()
{ {
var result = new CallResult<object>(new ServerError("TestError")); var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.Message, "TestError"); ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data); ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
@@ -71,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestCallResultErrorAs() public void TestCallResultErrorAs()
{ {
var result = new CallResult<TestObjectResult>(new ServerError("TestError")); var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.As<TestObject2>(default); var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError"); ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@@ -84,11 +86,11 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestCallResultErrorAsError() public void TestCallResultErrorAsError()
{ {
var result = new CallResult<TestObjectResult>(new ServerError("TestError")); var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@@ -97,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
[Test] [Test]
public void TestWebCallResultErrorAsError() public void TestWebCallResultErrorAsError()
{ {
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError")); var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@@ -112,6 +114,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new WebCallResult<TestObjectResult>( var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0], new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null, null,
@@ -124,10 +127,10 @@ namespace CryptoExchange.Net.UnitTests
ResultDataSource.Server, ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2")); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
Assert.That(asResult.Error.Message == "TestError2"); Assert.That(asResult.Error.ErrorCode == "TestError2");
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK); Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api"); Assert.That(asResult.RequestUrl == "https://test.com/api");
@@ -142,6 +145,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new WebCallResult<TestObjectResult>( var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new KeyValuePair<string, string[]>[0], new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null, null,
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)] [TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)] [TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)] [TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected) public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{ {
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input); var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
@@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null); Assert.That(result.Error != null);
Assert.That(result.Error is ServerError); Assert.That(result.Error is ServerError);
Assert.That(result.Error.Code == 123); Assert.That(result.Error.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request"); Assert.That(result.Error.Message == "Invalid request");
} }
@@ -182,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
[TestCase("/sapi/test1", true)] [TestCase("/sapi/test1", true)]
[TestCase("/sapi/test2", true)] [TestCase("/sapi/test2", true)]
[TestCase("/api/test1", false)] [TestCase("/api/test1", false)]
[TestCase("sapi/test1", false)] [TestCase("sapi/test1", true)]
[TestCase("/sapi/", true)] [TestCase("/sapi/", true)]
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting) public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
{ {
@@ -224,13 +224,17 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(null, null)] [TestCase(null, null)]
[TestCase("", null)] [TestCase("", null)]
[TestCase("null", null)] [TestCase("null", null)]
[TestCase("nan", null)]
[TestCase("1E+2", 100)] [TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)] [TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue [TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected) public void TestDecimalConverterString(string value, decimal? expected)
{ {
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}"); var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected)); Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
} }
[TestCase("1", 1)] [TestCase("1", 1)]
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using System; using System;
@@ -31,21 +32,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
internal class TestChannelQuery : Query<SubResponse> internal class TestChannelQuery : Query<SubResponse>
{ {
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight) public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{ {
ListenerIdentifiers = new HashSet<string> { request + "-" + channel }; MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
} }
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message) public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
{ {
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase)) if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{ {
return new CallResult<SubResponse>(new ServerError(message.Data.Status)); return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
} }
return base.HandleMessage(connection, message); return message.ToCallResult();
} }
} }
} }
@@ -9,11 +9,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{ {
internal class TestQuery : Query<object> internal class TestQuery : Query<object>
{ {
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight) public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{ {
ListenerIdentifiers = new HashSet<string> { identifier }; MessageMatcher = MessageMatcher.Create<object>(identifier);
} }
} }
} }
@@ -15,22 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{ {
private readonly Action<DataEvent<T>> _handler; private readonly Action<DataEvent<T>> _handler;
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false) public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{ {
_handler = handler; _handler = handler;
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
} }
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message) public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{ {
var data = (T)message.Data; _handler.Invoke(message);
_handler.Invoke(message.As(data));
return new CallResult(null); return new CallResult(null);
} }
public override Type GetMessageType(IMessageAccessor message) => typeof(T); protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1); protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
} }
} }
@@ -15,24 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
private readonly Action<DataEvent<T>> _handler; private readonly Action<DataEvent<T>> _handler;
private readonly string _channel; private readonly string _channel;
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false) public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
{ {
ListenerIdentifiers = new HashSet<string>() { channel }; MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
_handler = handler; _handler = handler;
_channel = channel; _channel = channel;
} }
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message) public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{ {
var data = (T)message.Data; _handler.Invoke(message);
_handler.Invoke(message.As(data));
return new CallResult(null); return new CallResult(null);
} }
public override Type GetMessageType(IMessageAccessor message) => typeof(T); protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1); protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
} }
} }
@@ -10,6 +10,7 @@ using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations;
@@ -55,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
var accessor = CreateAccessor(); var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result; var valid = accessor.Read(stream, true).Result;
if (!valid) if (!valid)
return new CallResult<T>(new ServerError(data)); return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
return deserializeResult; return deserializeResult;
@@ -77,10 +78,10 @@ namespace CryptoExchange.Net.UnitTests
{ {
} }
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat) public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{ {
} }
public string GetKey() => _credentials.Key; public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret; public string GetSecret() => _credentials.Secret;
} }
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Options;
using System.Linq; using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using CryptoExchange.Net.Objects.Errors;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -59,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray()); request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => .Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{ {
request.Setup(a => a.Uri).Returns(uri); request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method); request.Setup(a => a.Method).Returns(method);
@@ -68,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
.Returns(request.Object); .Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory); factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => .Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{ {
request.Setup(a => a.Uri).Returns(uri); request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method); request.Setup(a => a.Method).Returns(method);
@@ -89,12 +90,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we); request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object); .Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory); factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object); .Returns(request.Object);
} }
@@ -117,13 +118,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray()); request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object); .Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory); factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object); .Returns(request.Object);
} }
} }
@@ -197,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
var errorData = accessor.Deserialize<TestError>(); var errorData = accessor.Deserialize<TestError>();
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage); return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
} }
public override TimeSpan? GetTimeOffset() public override TimeSpan? GetTimeOffset()
@@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication
} }
/// <summary> /// <summary>
/// Authenticate a request. Output parameters should include the providedParameters input /// Authenticate a request
/// </summary> /// </summary>
/// <param name="apiClient">The Api client sending the request</param> /// <param name="apiClient">The Api client sending the request</param>
/// <param name="uri">The uri for the request</param> /// <param name="requestConfig">The request configuration</param>
/// <param name="method">The method of the request</param> public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="requestBodyFormat">The formatting of the request body</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>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
public abstract void AuthenticateRequest(
RestApiClient apiClient,
Uri uri,
HttpMethod method,
ref IDictionary<string, object>? uriParameters,
ref IDictionary<string, object>? bodyParameters,
ref Dictionary<string, string>? headers,
bool auth,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parameterPosition,
RequestBodyFormat requestBodyFormat
);
/// <summary> /// <summary>
/// SHA256 sign the data and return the bytes /// SHA256 sign the data and return the bytes
@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -54,6 +56,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public ExchangeOptions ClientOptions { get; } public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -87,6 +94,16 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc /> /// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null); public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc /> /// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{ {
+86 -77
View File
@@ -11,6 +11,7 @@ using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
@@ -54,7 +55,7 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Request headers to be sent with each request /// Request headers to be sent with each request
/// </summary> /// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; } protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary> /// <summary>
/// Whether parameters need to be ordered /// Whether parameters need to be ordered
@@ -105,7 +106,7 @@ namespace CryptoExchange.Net.Clients
options, options,
apiOptions) apiOptions)
{ {
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); RequestFactory.Configure(options, httpClient);
} }
/// <summary> /// <summary>
@@ -238,7 +239,7 @@ namespace CryptoExchange.Net.Clients
additionalHeaders); additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++; TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); var result = await GetResponseAsync<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (result.Error is not CancellationRequestedError) if (result.Error is not CancellationRequestedError)
{ {
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
@@ -363,74 +364,58 @@ namespace CryptoExchange.Net.Clients
ParameterCollection? bodyParameters, ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders) Dictionary<string, string>? additionalHeaders)
{ {
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters); var requestConfiguration = new RestRequestConfiguration(
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters); definition,
baseAddress,
uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
new Dictionary<string, string>(additionalHeaders ?? []),
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
var uri = new Uri(baseAddress.AppendPath(definition.Path)); try
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
Dictionary<string, string>? headers = null;
if (AuthenticationProvider != null)
{ {
try AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
definition.Method,
ref uriParams,
ref bodyParams,
ref headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat
);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
} }
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
var queryString = requestConfiguration.GetQueryString(true);
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
queryString = $"?{queryString}";
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
if (uriParams != null) var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
if (headers != null) foreach (var header in requestConfiguration.Headers)
{ request.AddHeader(header.Key, header.Value);
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
}
if (additionalHeaders != null) foreach (var header in StandardRequestHeaders)
{ {
foreach (var header in additionalHeaders) // Only add it if it isn't overwritten
if (!requestConfiguration.Headers.ContainsKey(header.Key))
request.AddHeader(header.Key, header.Value); request.AddHeader(header.Key, header.Value);
} }
if (StandardRequestHeaders != null) if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
{ {
foreach (var header in StandardRequestHeaders) var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
var bodyContent = requestConfiguration.GetBodyContent();
if (bodyContent != null)
{ {
// Only add it if it isn't overwritten request.SetContent(bodyContent, contentType);
if (additionalHeaders?.ContainsKey(header.Key) != true)
request.AddHeader(header.Key, header.Value);
} }
}
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParams != null && bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); {
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
} }
return request; return request;
@@ -439,11 +424,13 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Executes the request and returns the result deserialized into the type parameter class /// Executes the request and returns the result deserialized into the type parameter class
/// </summary> /// </summary>
/// <param name="requestDefinition">The request definition</param>
/// <param name="request">The request object to execute</param> /// <param name="request">The request object to execute</param>
/// <param name="gate">The ratelimit gate used</param> /// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>( protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
RequestDefinition requestDefinition,
IRequest request, IRequest request,
IRateLimitGate? gate, IRateLimitGate? gate,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -456,14 +443,11 @@ namespace CryptoExchange.Net.Clients
{ {
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop(); sw.Stop();
var statusCode = response.StatusCode;
var headers = response.ResponseHeaders;
var responseLength = response.ContentLength;
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
accessor = CreateAccessor(); accessor = CreateAccessor();
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{ {
// Error response // Error response
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
@@ -488,23 +472,22 @@ namespace CryptoExchange.Net.Clients
if (error.Code == null || error.Code == 0) if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode; error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!); return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
} }
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (typeof(T) == typeof(object)) if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct // Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
if (!valid) if (!valid)
{ {
// Invalid json // Invalid json
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception); return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
// Json response received // Json response received
var parsedError = TryParseError(response.ResponseHeaders, accessor); var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
if (parsedError != null) if (parsedError != null)
{ {
if (parsedError is ServerRateLimitError rateError) if (parsedError is ServerRateLimitError rateError)
@@ -517,30 +500,55 @@ namespace CryptoExchange.Net.Clients
} }
// Success status code, but TryParseError determined it was an error response // Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
} }
var deserializeResult = accessor.Deserialize<T>(); var deserializeResult = accessor.Deserialize<T>();
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error); return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
} }
catch (HttpRequestException requestException) catch (HttpRequestException requestException)
{ {
// Request exception, can't reach server for instance // Request exception, can't reach server for instance
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException)); var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
catch (OperationCanceledException canceledException) catch (OperationCanceledException canceledException)
{ {
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{ {
// Cancellation token canceled by caller // Cancellation token canceled by caller
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
} }
else else
{ {
// Request timed out // Request timed out
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException)); var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
} }
catch (ArgumentException argumentException)
{
if (argumentException.Message.StartsWith("Only HTTP/"))
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
catch (NotSupportedException notSupportedException)
{
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
finally finally
{ {
accessor?.Clear(); accessor?.Clear();
@@ -554,10 +562,11 @@ namespace CryptoExchange.Net.Clients
/// This method will be called for each response to be able to check if the response is an error or not. /// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null /// If the response is an error this method should return the parsed error, else it should return null
/// </summary> /// </summary>
/// <param name="requestDefinition">Request definition</param>
/// <param name="accessor">Data accessor</param> /// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param> /// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns> /// <returns>Null if not an error, Error otherwise</returns>
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null; protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
/// <summary> /// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
@@ -633,7 +642,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception) protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{ {
return new ServerError(null, "Unknown request error", exception); return new ServerError(ErrorInfo.Unknown, exception);
} }
/// <summary> /// <summary>
@@ -684,21 +693,21 @@ namespace CryptoExchange.Net.Clients
{ {
base.SetOptions(options); base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout); RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
} }
internal async Task<WebCallResult<bool>> SyncTimeAsync() internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
var timeSyncParams = GetTimeSyncInfo(); var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null) if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{ {
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
{ {
timeSyncParams.TimeSyncState.Semaphore.Release(); timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
} }
var localTime = DateTime.UtcNow; var localTime = DateTime.UtcNow;
@@ -727,7 +736,7 @@ namespace CryptoExchange.Net.Clients
timeSyncParams.TimeSyncState.Semaphore.Release(); timeSyncParams.TimeSyncState.Semaphore.Release();
} }
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null); return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
} }
private bool ShouldCache(RequestDefinition definition) private bool ShouldCache(RequestDefinition definition)
+59 -21
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
@@ -82,6 +83,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true; protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary>
/// Whether to continue processing and forward unparsable messages to handlers
/// </summary>
protected internal bool ProcessUnparsableMessages { get; set; } = false;
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
{ {
@@ -221,7 +227,7 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
// Get a new or existing socket connection // Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<UpdateSubscription>(null); return socketResult.As<UpdateSubscription>(null);
@@ -260,11 +266,11 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused")); return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
var waitEvent = new AsyncResetEvent(false); var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection); var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null) if (subQuery != null)
{ {
// Send the request and wait for answer // Send the request and wait for answer
@@ -308,11 +314,10 @@ namespace CryptoExchange.Net.Clients
/// Send a query on a socket connection to the BaseAddress and wait for the response /// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary> /// </summary>
/// <typeparam name="THandlerResponse">Expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default) protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
{ {
return QueryAsync(BaseAddress, query, ct); return QueryAsync(BaseAddress, query, ct);
} }
@@ -321,12 +326,11 @@ namespace CryptoExchange.Net.Clients
/// Send a query on a socket connection and wait for the response /// Send a query on a socket connection and wait for the response
/// </summary> /// </summary>
/// <typeparam name="THandlerResponse">Expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="url">The url for the request</param> /// <param name="url">The url for the request</param>
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default) protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
{ {
if (_disposing) if (_disposing)
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query")); return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
@@ -339,7 +343,7 @@ namespace CryptoExchange.Net.Clients
await semaphoreSlim.WaitAsync().ConfigureAwait(false); await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try try
{ {
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<THandlerResponse>(default); return socketResult.As<THandlerResponse>(default);
@@ -365,7 +369,7 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<THandlerResponse>(new ServerError("Socket is paused")); return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
@@ -490,25 +494,56 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param> /// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param> /// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param> /// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="ct">Cancellation token</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param> /// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null) protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
{ {
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected) var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType() && s.Value.ApiClient.GetType() == GetType()
&& (s.Value.Authenticated == authenticated || !authenticated) && (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)) .Select(x => x.Value)
&& s.Value.Connected); .ToList();
SocketConnection connection; // If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError());
}
break;
}
delayed = true;
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError());
}
if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList();
SocketConnection? connection;
if (!dedicatedRequestConnection) if (!dedicatedRequestConnection)
{ {
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value; connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
} }
else else
{ {
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value; connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated) if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated // Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated; connection.DedicatedRequestConnection.Authenticated = authenticated;
@@ -516,9 +551,12 @@ namespace CryptoExchange.Net.Clients
if (connection != null) if (connection != null)
{ {
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))) if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new // Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(connection); return new CallResult<SocketConnection>(connection);
}
} }
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
@@ -712,7 +750,7 @@ namespace CryptoExchange.Net.Clients
{ {
foreach (var item in DedicatedConnectionConfigs) foreach (var item in DedicatedConnectionConfigs)
{ {
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false); var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.AsDataless(); return socketResult.AsDataless();
@@ -811,7 +849,7 @@ namespace CryptoExchange.Net.Clients
sb.AppendLine($"\t\t\tId: {subState.Id}"); sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}"); sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}"); sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{string.Join(",", subState.Identifiers)}]"); sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
}); });
} }
}); });
@@ -140,10 +140,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
}; };
if (string.IsNullOrEmpty(stringValue)) if (stringValue is null)
return null; return null;
if (!GetValue(enumType, stringValue!, out var result)) if (!GetValue(enumType, stringValue, out var result))
{ {
if (string.IsNullOrWhiteSpace(stringValue)) if (string.IsNullOrWhiteSpace(stringValue))
{ {
@@ -204,6 +204,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false; return false;
} }
if (String.IsNullOrEmpty(value))
{
// An empty/null value will always fail when parsing, so just return here
result = default;
return false;
}
try try
{ {
// If no explicit mapping is found try to parse string // If no explicit mapping is found try to parse string
@@ -60,13 +60,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
catch (JsonException ex) catch (JsonException ex)
{ {
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<object>(new DeserializeError(info, ex)); return new CallResult<object>(new DeserializeError(info, ex));
} }
catch (Exception ex) catch (Exception ex)
{ {
var info = $"Deserialize unknown Exception: {ex.Message}"; return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
return new CallResult<object>(new DeserializeError(info, ex));
} }
} }
@@ -87,13 +86,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
catch (JsonException ex) catch (JsonException ex)
{ {
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, ex)); return new CallResult<T>(new DeserializeError(info, ex));
} }
catch (Exception ex) catch (Exception ex)
{ {
var info = $"Unknown exception: {ex.Message}"; return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
return new CallResult<T>(new DeserializeError(info, ex));
} }
} }
@@ -286,7 +284,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
// Not a json message // Not a json message
IsValid = false; IsValid = false;
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
} }
} }
@@ -338,7 +336,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow // Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
IsValid = false; IsValid = false;
return new CallResult(new ServerError("Not a json value")); return new CallResult(new DeserializeError("Not a json value"));
} }
_document = JsonDocument.Parse(data); _document = JsonDocument.Parse(data);
@@ -349,7 +347,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
// Not a json message // Not a json message
IsValid = false; IsValid = false;
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
} }
} }
+4 -4
View File
@@ -6,11 +6,11 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.2.0</PackageVersion> <PackageVersion>9.8.0</PackageVersion>
<AssemblyVersion>9.2.0</AssemblyVersion> <AssemblyVersion>9.8.0</AssemblyVersion>
<FileVersion>9.2.0</FileVersion> <FileVersion>9.8.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl> <RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
+32 -14
View File
@@ -88,8 +88,6 @@ namespace CryptoExchange.Net
value -= offset; value -= offset;
else value += (step.Value - offset); else value += (step.Value - offset);
} }
value = RoundDown(value, 8);
return value.Normalize(); return value.Normalize();
} }
@@ -348,22 +346,42 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public static decimal? ParseDecimal(string? value) public static decimal? ParseDecimal(string? value)
{ {
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase)) // Value is null or empty is the most common case to return null so check before trying to parse
if (string.IsNullOrEmpty(value))
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
return decValue;
// Check for values which should be parsed to null
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
{
return null; return null;
if (string.Equals("Infinity", value, StringComparison.Ordinal))
// Infinity returned by the server, default to max value
return decimal.MaxValue;
try
{
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
} }
catch (OverflowException)
{ // Infinity value should be parsed to min/max value
// Value doesn't fit decimal, default to max value if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue; return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
// assume overflow
if (overflowValue < 0)
return decimal.MinValue;
else
return decimal.MaxValue;
} }
// Unknown decimal format, return null
return null;
} }
} }
} }
+2 -2
View File
@@ -23,14 +23,14 @@ namespace CryptoExchange.Net
{ {
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
{ {
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name })); exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
_symbolInfos.TryAdd(topicId, exchangeInfo); _symbolInfos.TryAdd(topicId, exchangeInfo);
} }
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60)) if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
return; return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name })); _symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
} }
/// <summary> /// <summary>
+2 -2
View File
@@ -498,8 +498,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IBookTickerSocketClient)client(x)!); services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T))) if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineSocketClient)client(x)!); services.AddTransient(x => (IKlineSocketClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T))) if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!); services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T))) if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITickerSocketClient)client(x)!); services.AddTransient(x => (ITickerSocketClient)client(x)!);
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T))) if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
@@ -17,22 +17,13 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
public int Id { get; } public int Id { get; }
/// <summary> /// <summary>
/// The identifiers for this processor /// The matcher for this listener
/// </summary> /// </summary>
public HashSet<string> ListenerIdentifiers { get; } public MessageMatcher MessageMatcher { get; }
/// <summary> /// <summary>
/// Handle a message /// Handle a message
/// </summary> /// </summary>
/// <param name="connection"></param> Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matchedHandler);
/// <param name="message"></param>
/// <returns></returns>
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
Type? GetMessageType(IMessageAccessor messageAccessor);
/// <summary> /// <summary>
/// Deserialize a message into object of type /// Deserialize a message into object of type
/// </summary> /// </summary>
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
Uri Uri { get; } Uri Uri { get; }
/// <summary> /// <summary>
/// HTTP protocol version
/// </summary>
Version HttpVersion { get; }
/// <summary>
/// internal request id for tracing /// internal request id for tracing
/// </summary> /// </summary>
int RequestId { get; } int RequestId { get; }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System; using System;
using System.Net.Http; using System.Net.Http;
@@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces
/// <summary> /// <summary>
/// Create a request for an uri /// Create a request for an uri
/// </summary> /// </summary>
/// <param name="method"></param> IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId);
/// <param name="uri"></param>
/// <param name="requestId"></param>
/// <returns></returns>
IRequest Create(HttpMethod method, Uri uri, int requestId);
/// <summary> /// <summary>
/// Configure the requests created by this factory /// Configure the requests created by this factory
/// </summary> /// </summary>
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="options">Rest client options</param>
/// <param name="httpClient">Optional shared http client instance</param> /// <param name="httpClient">Optional shared http client instance</param>
/// <param name="proxy">Optional proxy to use when no http client is provided</param> void Configure(RestExchangeOptions options, HttpClient? httpClient = null);
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
/// <summary> /// <summary>
/// Update settings /// Update settings
/// </summary> /// </summary>
/// <param name="proxy">Proxy to use</param> /// <param name="proxy">Proxy to use</param>
/// <param name="requestTimeout">Request timeout to use</param> /// <param name="requestTimeout">Request timeout to use</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout); /// <param name="httpKeepAliveInterval">Http client keep alive interval</param>
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval);
} }
} }
+7 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
HttpStatusCode StatusCode { get; } HttpStatusCode StatusCode { get; }
/// <summary>
/// Http protocol version
/// </summary>
Version HttpVersion { get; }
/// <summary> /// <summary>
/// Whether the status code indicates a success status /// Whether the status code indicates a success status
/// </summary> /// </summary>
@@ -0,0 +1,45 @@
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.Klines;
using CryptoExchange.Net.Trackers.Trades;
using System;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Tracker factory
/// </summary>
public interface ITrackerFactory
{
/// <summary>
/// Whether the factory supports creating a KlineTracker instance for this symbol and interval
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="interval">The kline interval</param>
bool CanCreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval);
/// <summary>
/// Create a new kline tracker
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="interval">Kline interval</param>
/// <param name="limit">The max amount of klines to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <returns></returns>
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
/// <summary>
/// Whether the factory supports creating a TradeTracker instance for this symbol
/// </summary>
/// <param name="symbol">The symbol</param>
bool CanCreateTradeTracker(SharedSymbol symbol);
/// <summary>
/// Create a new trade tracker for a symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <param name="limit">The max amount of trades to retain</param>
/// <param name="period">The max period the data should be retained</param>
/// <returns></returns>
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
}
}
+57 -1
View File
@@ -1,5 +1,8 @@
using System; using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text; using System.Text;
namespace CryptoExchange.Net namespace CryptoExchange.Net
@@ -43,5 +46,58 @@ namespace CryptoExchange.Net
return clientOrderId; return clientOrderId;
} }
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
{
#if NET5_0_OR_GREATER
var socketHandler = new SocketsHttpHandler();
try
{
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
{
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
}
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
socketHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
return socketHandler;
#else
var httpHandler = new HttpClientHandler();
try
{
httpHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
httpHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
httpHandler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
return httpHandler;
#endif
}
} }
} }
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription; private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections; private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard; private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
private static readonly Action<ILogger, Exception?> _timeoutWaitingForReconnectingSocket;
private static readonly Action<ILogger, long, Exception?> _waitedForReconnectingSocket;
static SocketApiClientLoggingExtension() static SocketApiClientLoggingExtension()
{ {
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Warning, LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"), new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited"); "Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
"Timeout while waiting for existing socket reconnection, failing request");
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
LogLevel.Trace,
new EventId(3020, "WaitedForReconnectingSocket"),
"Waited for reconnecting socket for {Timespan}ms");
} }
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId) public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_addingRetryAfterGuard(logger, retryAfter, null); _addingRetryAfterGuard(logger, retryAfter, null);
} }
public static void TimeoutWaitingForReconnectingSocket(this ILogger logger)
{
_timeoutWaitingForReconnectingSocket(logger, null);
}
public static void WaitedForReconnectingSocket(this ILogger logger, long milliseconds)
{
_waitedForReconnectingSocket(logger, milliseconds, null);
}
} }
} }
@@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _failedToParse; private static readonly Action<ILogger, int, string, Exception?> _failedToParse;
private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage; private static readonly Action<ILogger, int, string, Exception?> _failedToEvaluateMessage;
private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage; private static readonly Action<ILogger, int, Exception?> _errorProcessingMessage;
private static readonly Action<ILogger, int, int, string, Exception?> _processorMatched; private static readonly Action<ILogger, int, string, string, Exception?> _processorMatched;
private static readonly Action<ILogger, int, int, Exception?> _receivedMessageNotRecognized; private static readonly Action<ILogger, int, int, Exception?> _receivedMessageNotRecognized;
private static readonly Action<ILogger, int, string?, Exception?> _failedToDeserializeMessage; private static readonly Action<ILogger, int, string?, Exception?> _failedToDeserializeMessage;
private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed; private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed;
@@ -92,11 +92,6 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2009, "ErrorProcessingMessage"), new EventId(2009, "ErrorProcessingMessage"),
"[Sckt {SocketId}] error processing message"); "[Sckt {SocketId}] error processing message");
_processorMatched = LoggerMessage.Define<int, int, string>(
LogLevel.Trace,
new EventId(2010, "ProcessorMatched"),
"[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}");
_receivedMessageNotRecognized = LoggerMessage.Define<int, int>( _receivedMessageNotRecognized = LoggerMessage.Define<int, int>(
LogLevel.Warning, LogLevel.Warning,
new EventId(2011, "ReceivedMessageNotRecognized"), new EventId(2011, "ReceivedMessageNotRecognized"),
@@ -190,7 +185,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>( _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning, LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"), new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}"); "[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
_failedToParse = LoggerMessage.Define<int, string>( _failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning, LogLevel.Warning,
@@ -201,6 +196,12 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Trace, LogLevel.Trace,
new EventId(2031, "SendingByteData"), new EventId(2031, "SendingByteData"),
"[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}"); "[Sckt {SocketId}] [Req {RequestId}] sending byte message of length: {Length}");
_processorMatched = LoggerMessage.Define<int, string, string>(
LogLevel.Trace,
new EventId(2032, "ProcessorMatched"),
"[Sckt {SocketId}] listener '{ListenId}' matched to message with listener identifier {ListenerId}");
} }
public static void ActivityPaused(this ILogger logger, int socketId, bool paused) public static void ActivityPaused(this ILogger logger, int socketId, bool paused)
@@ -256,9 +257,9 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_errorProcessingMessage(logger, socketId, e); _errorProcessingMessage(logger, socketId, e);
} }
public static void ProcessorMatched(this ILogger logger, int socketId, int count, string listenerId) public static void ProcessorMatched(this ILogger logger, int socketId, string listener, string listenerId)
{ {
_processorMatched(logger, socketId, count, listenerId, null); _processorMatched(logger, socketId, listener, listenerId, null);
} }
public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id) public static void ReceivedMessageNotRecognized(this ILogger logger, int socketId, int id)
{ {
+24 -23
View File
@@ -205,6 +205,11 @@ namespace CryptoExchange.Net.Objects
/// The request http method /// The request http method
/// </summary> /// </summary>
public HttpMethod? RequestMethod { get; set; } public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary> /// <summary>
/// The headers sent with the request /// The headers sent with the request
@@ -251,6 +256,7 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public WebCallResult( public WebCallResult(
HttpStatusCode? code, HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders, KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime, TimeSpan? responseTime,
string? originalData, string? originalData,
@@ -262,6 +268,7 @@ namespace CryptoExchange.Net.Objects
Error? error) : base(error) Error? error) : base(error)
{ {
ResponseStatusCode = code; ResponseStatusCode = code;
HttpVersion = httpVersion;
ResponseHeaders = responseHeaders; ResponseHeaders = responseHeaders;
ResponseTime = responseTime; ResponseTime = responseTime;
RequestId = requestId; RequestId = requestId;
@@ -286,7 +293,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public WebCallResult AsError(Error error) public WebCallResult AsError(Error error)
{ {
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
} }
/// <summary> /// <summary>
@@ -297,7 +304,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public WebCallResult<K> As<K>([AllowNull] K data) public WebCallResult<K> As<K>([AllowNull] K data)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error); return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
} }
/// <summary> /// <summary>
@@ -334,7 +341,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public WebCallResult<K> AsError<K>(Error error) public WebCallResult<K> AsError<K>(Error error)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error); return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -355,6 +362,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public HttpMethod? RequestMethod { get; set; } public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary> /// <summary>
/// The headers sent with the request /// The headers sent with the request
/// </summary> /// </summary>
@@ -403,21 +415,9 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// Create a new result /// Create a new result
/// </summary> /// </summary>
/// <param name="code"></param>
/// <param name="responseHeaders"></param>
/// <param name="responseTime"></param>
/// <param name="responseLength"></param>
/// <param name="originalData"></param>
/// <param name="requestId"></param>
/// <param name="requestUrl"></param>
/// <param name="requestBody"></param>
/// <param name="requestMethod"></param>
/// <param name="requestHeaders"></param>
/// <param name="dataSource"></param>
/// <param name="data"></param>
/// <param name="error"></param>
public WebCallResult( public WebCallResult(
HttpStatusCode? code, HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders, KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime, TimeSpan? responseTime,
long? responseLength, long? responseLength,
@@ -431,6 +431,7 @@ namespace CryptoExchange.Net.Objects
[AllowNull] T data, [AllowNull] T data,
Error? error) : base(data, originalData, error) Error? error) : base(data, originalData, error)
{ {
HttpVersion = httpVersion;
ResponseStatusCode = code; ResponseStatusCode = code;
ResponseHeaders = responseHeaders; ResponseHeaders = responseHeaders;
ResponseTime = responseTime; ResponseTime = responseTime;
@@ -450,7 +451,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult AsDataless() public new WebCallResult AsDataless()
{ {
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
} }
/// <summary> /// <summary>
/// Copy as a dataless result /// Copy as a dataless result
@@ -458,14 +459,14 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult AsDatalessError(Error error) public new WebCallResult AsDatalessError(Error error)
{ {
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
} }
/// <summary> /// <summary>
/// Create a new error result /// Create a new error result
/// </summary> /// </summary>
/// <param name="error">The error</param> /// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { } public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary> /// <summary>
/// Copy the WebCallResult to a new data type /// Copy the WebCallResult to a new data type
@@ -475,7 +476,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data) public new WebCallResult<K> As<K>([AllowNull] K data)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error); return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
} }
/// <summary> /// <summary>
@@ -486,7 +487,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error) public new WebCallResult<K> AsError<K>(Error error)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
} }
/// <summary> /// <summary>
@@ -498,7 +499,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data) public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
{ {
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
} }
/// <summary> /// <summary>
@@ -569,7 +570,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
internal WebCallResult<T> Cached() internal WebCallResult<T> Cached()
{ {
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error); return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
} }
/// <inheritdoc /> /// <inheritdoc />
+16
View File
@@ -251,4 +251,20 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
DEX DEX
} }
/// <summary>
/// Timeout behavior for queries
/// </summary>
public enum TimeoutBehavior
{
/// <summary>
/// Fail the request
/// </summary>
Fail,
/// <summary>
/// Mark the query as successful
/// </summary>
Succeed
}
} }
+148 -57
View File
@@ -1,4 +1,5 @@
using System; using CryptoExchange.Net.Objects.Errors;
using System;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
{ {
@@ -7,15 +8,52 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public abstract class Error public abstract class Error
{ {
private int? _code;
/// <summary> /// <summary>
/// The error code from the server /// The int error code the server returned; or the http status code int value if there was no error code.<br />
/// <br />
/// <i>Note:</i><br />
/// The <see cref="ErrorCode"/> property should be used for more generic error checking; it might contain a string error code if the server does not return an int code.
/// </summary> /// </summary>
public int? Code { get; set; } public int? Code
{
get
{
if (_code.HasValue)
return _code;
return int.TryParse(ErrorCode, out var r) ? r : null;
}
set
{
_code = value;
}
}
/// <summary> /// <summary>
/// The message for the error that occurred /// The error code returned by the server
/// </summary> /// </summary>
public string Message { get; set; } public string? ErrorCode { get; set; }
/// <summary>
/// The error description
/// </summary>
public string? ErrorDescription { get; set; }
/// <summary>
/// Error type
/// </summary>
public ErrorType ErrorType { get; set; }
/// <summary>
/// Whether the error is transient and can be retried
/// </summary>
public bool IsTransient { get; set; }
/// <summary>
/// The server message for the error that occurred
/// </summary>
public string? Message { get; set; }
/// <summary> /// <summary>
/// Underlying exception /// Underlying exception
@@ -25,10 +63,13 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected Error (int? code, string message, Exception? exception) protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception)
{ {
Code = code; ErrorCode = errorCode;
Message = message; ErrorType = errorInfo.ErrorType;
Message = errorInfo.Message;
ErrorDescription = errorInfo.ErrorDescription;
IsTransient = errorInfo.IsTransient;
Exception = exception; Exception = exception;
} }
@@ -38,7 +79,7 @@ namespace CryptoExchange.Net.Objects
/// <returns></returns> /// <returns></returns>
public override string ToString() public override string ToString()
{ {
return Code != null ? $"[{GetType().Name}] {Code}: {Message}" : $"[{GetType().Name}] {Message}"; return ErrorCode != null ? $"[{GetType().Name}.{ErrorType}] {ErrorCode}: {Message ?? ErrorDescription}" : $"[{GetType().Name}.{ErrorType}] {Message ?? ErrorDescription}";
} }
} }
@@ -48,19 +89,24 @@ namespace CryptoExchange.Net.Objects
public class CantConnectError : Error public class CantConnectError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public CantConnectError() : base(null, "Can't connect to the server", null) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.UnableToConnect, false, "Can't connect to the server");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { } public CantConnectError() : base(null, _errorInfo, null) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { } public CantConnectError(Exception? exception) : base(null, _errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
protected CantConnectError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -69,14 +115,19 @@ namespace CryptoExchange.Net.Objects
public class NoApiCredentialsError : Error public class NoApiCredentialsError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false, "No credentials provided for private endpoint");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { } public NoApiCredentialsError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected NoApiCredentialsError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -87,12 +138,19 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public ServerError(string message) : base(null, message, null) { } public ServerError(ErrorInfo errorInfo, Exception? exception = null)
: base(null, errorInfo, exception) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public ServerError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public ServerError(int errorCode, ErrorInfo errorInfo, Exception? exception = null)
: this(errorCode.ToString(), errorInfo, exception) { }
/// <summary>
/// ctor
/// </summary>
public ServerError(string errorCode, ErrorInfo errorInfo, Exception? exception = null) : base(errorCode, errorInfo, exception) { }
} }
/// <summary> /// <summary>
@@ -101,14 +159,30 @@ namespace CryptoExchange.Net.Objects
public class WebError : Error public class WebError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public WebError(string message, Exception? exception = null) : base(null, message, exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.NetworkError, true, "Failed to complete the request to the server due to a network error");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public WebError(int code, string message, Exception? exception = null) : base(code, message, exception) { } public WebError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
/// Timeout error waiting for a response from the server
/// </summary>
public class TimeoutError : Error
{
/// <summary>
/// Default error info
/// </summary>
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.Timeout, false, "Failed to receive a response from the server in time");
/// <summary>
/// ctor
/// </summary>
public TimeoutError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
} }
/// <summary> /// <summary>
@@ -117,30 +191,14 @@ namespace CryptoExchange.Net.Objects
public class DeserializeError : Error public class DeserializeError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public DeserializeError(string message, Exception? exception = null) : base(null, message, exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.DeserializationFailed, false, "Failed to deserialize data");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
}
/// <summary>
/// Unknown error
/// </summary>
public class UnknownError : Error
{
/// <summary>
/// ctor
/// </summary>
public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { }
/// <summary>
/// ctor
/// </summary>
protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { }
} }
/// <summary> /// <summary>
@@ -149,14 +207,28 @@ namespace CryptoExchange.Net.Objects
public class ArgumentError : Error public class ArgumentError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info for missing parameter
/// </summary> /// </summary>
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { } protected static readonly ErrorInfo _missingInfo = new ErrorInfo(ErrorType.MissingParameter, false, "Missing parameter");
/// <summary>
/// Default error info for invalid parameter
/// </summary>
protected static readonly ErrorInfo _invalidInfo = new ErrorInfo(ErrorType.InvalidParameter, false, "Invalid parameter");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected ArgumentError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public static ArgumentError Missing(string parameterName, string? message = null) => new ArgumentError(_missingInfo with { Message = message == null ? $"{_missingInfo.Message} '{parameterName}'" : $"{_missingInfo.Message} '{parameterName}': {message}" }, null);
/// <summary>
/// ctor
/// </summary>
public static ArgumentError Invalid(string parameterName, string message) => new ArgumentError(_invalidInfo with { Message = $"{_invalidInfo.Message} '{parameterName}': {message}" }, null);
/// <summary>
/// ctor
/// </summary>
protected ArgumentError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -172,7 +244,7 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { } protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
} }
/// <summary> /// <summary>
@@ -181,15 +253,19 @@ namespace CryptoExchange.Net.Objects
public class ClientRateLimitError : BaseRateLimitError public class ClientRateLimitError : BaseRateLimitError
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
/// <param name="message"></param> protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded");
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected ClientRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public ClientRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
/// <summary>
/// ctor
/// </summary>
protected ClientRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
} }
/// <summary> /// <summary>
@@ -198,14 +274,19 @@ namespace CryptoExchange.Net.Objects
public class ServerRateLimitError : BaseRateLimitError public class ServerRateLimitError : BaseRateLimitError
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public ServerRateLimitError(string? message = null, Exception? exception = null) : base(null, "Server rate limit exceeded" + (message?.Length > 0 ? " : " + message : null), exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Server rate limit exceeded");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected ServerRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public ServerRateLimitError(string? message = null, Exception? exception = null) : base(_errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
/// <summary>
/// ctor
/// </summary>
protected ServerRateLimitError(ErrorInfo info, Exception? exception) : base(info, exception) { }
} }
/// <summary> /// <summary>
@@ -214,14 +295,19 @@ namespace CryptoExchange.Net.Objects
public class CancellationRequestedError : Error public class CancellationRequestedError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public CancellationRequestedError(Exception? exception = null) : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
protected CancellationRequestedError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
/// <summary> /// <summary>
@@ -230,13 +316,18 @@ namespace CryptoExchange.Net.Objects
public class InvalidOperationError : Error public class InvalidOperationError : Error
{ {
/// <summary> /// <summary>
/// ctor /// Default error info
/// </summary> /// </summary>
public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { } protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.InvalidOperation, false, "Operation invalid");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } public InvalidOperationError(string message) : base(null, _errorInfo with { Message = message }, null) { }
/// <summary>
/// ctor
/// </summary>
protected InvalidOperationError(ErrorInfo info, Exception? exception) : base(null, info, exception) { }
} }
} }
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error evaluator
/// </summary>
public class ErrorEvaluator
{
/// <summary>
/// Error code
/// </summary>
public string[] ErrorCodes { get; set; }
/// <summary>
/// Evaluation callback for determining the error type
/// </summary>
public Func<string, string?, ErrorInfo> ErrorTypeEvaluator { get; set; }
/// <summary>
/// ctor
/// </summary>
public ErrorEvaluator(string errorCode, Func<string, string?, ErrorInfo> errorTypeEvaluator)
{
ErrorCodes = [errorCode];
ErrorTypeEvaluator = errorTypeEvaluator;
}
/// <summary>
/// ctor
/// </summary>
public ErrorEvaluator(string[] errorCodes, Func<string, string?, ErrorInfo> errorTypeEvaluator)
{
ErrorCodes = errorCodes;
ErrorTypeEvaluator = errorTypeEvaluator;
}
}
}
@@ -0,0 +1,58 @@
using System;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error info
/// </summary>
public record ErrorInfo
{
/// <summary>
/// Unknown error info
/// </summary>
public static ErrorInfo Unknown { get; } = new ErrorInfo(ErrorType.Unknown, false, "Unknown error", []);
/// <summary>
/// The server error code
/// </summary>
public string[] ErrorCodes { get; set; }
/// <summary>
/// Error description
/// </summary>
public string? ErrorDescription { get; set; }
/// <summary>
/// The error type
/// </summary>
public ErrorType ErrorType { get; set; }
/// <summary>
/// Whether the error is transient and can be retried
/// </summary>
public bool IsTransient { get; set; }
/// <summary>
/// Server response message
/// </summary>
public string? Message { get; set; }
/// <summary>
/// ctor
/// </summary>
public ErrorInfo(ErrorType errorType, string description)
{
ErrorCodes = [];
ErrorType = errorType;
IsTransient = false;
ErrorDescription = description;
}
/// <summary>
/// ctor
/// </summary>
public ErrorInfo(ErrorType errorType, bool isTransient, string description, params string[] errorCodes)
{
ErrorCodes = errorCodes;
ErrorType = errorType;
IsTransient = isTransient;
ErrorDescription = description;
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Error mapping collection
/// </summary>
public class ErrorMapping
{
private Dictionary<string, ErrorEvaluator> _evaluators = new Dictionary<string, ErrorEvaluator>();
private Dictionary<string, ErrorInfo> _directMapping = new Dictionary<string, ErrorInfo>();
/// <summary>
/// ctor
/// </summary>
public ErrorMapping(ErrorInfo[] errorMappings, ErrorEvaluator[]? errorTypeEvaluators = null)
{
foreach (var item in errorMappings)
{
if (!item.ErrorCodes.Any())
throw new Exception("Error codes can't be null in error mapping");
foreach(var code in item.ErrorCodes!)
_directMapping.Add(code, item);
}
if (errorTypeEvaluators == null)
return;
foreach (var item in errorTypeEvaluators)
{
foreach(var code in item.ErrorCodes)
_evaluators.Add(code, item);
}
}
/// <summary>
/// Get error info for an error code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message)
{
if (_directMapping.TryGetValue(code!, out var info))
return info with { Message = message };
if (_evaluators.TryGetValue(code!, out var eva))
return eva.ErrorTypeEvaluator.Invoke(code!, message) with { Message = message };
return ErrorInfo.Unknown with { Message = message };
}
}
}
@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects.Errors
{
/// <summary>
/// Type of error
/// </summary>
public enum ErrorType
{
#region Library errors
/// <summary>
/// Failed to connect to server
/// </summary>
UnableToConnect,
/// <summary>
/// Failed to complete the request to the server
/// </summary>
NetworkError,
/// <summary>
/// No API credentials have been specified
/// </summary>
MissingCredentials,
/// <summary>
/// Invalid parameter value
/// </summary>
InvalidParameter,
/// <summary>
/// Missing parameter value
/// </summary>
MissingParameter,
/// <summary>
/// Cancellation requested by user
/// </summary>
CancellationRequested,
/// <summary>
/// Invalid operation requested
/// </summary>
InvalidOperation,
/// <summary>
/// Failed to deserialize data
/// </summary>
DeserializationFailed,
/// <summary>
/// Websocket is temporarily paused
/// </summary>
WebsocketPaused,
/// <summary>
/// Timeout while waiting for data from the order book subscription
/// </summary>
OrderBookTimeout,
/// <summary>
/// All orders failed for a multi-order operation
/// </summary>
AllOrdersFailed,
/// <summary>
/// Request timeout
/// </summary>
Timeout,
#endregion
#region Server errors
/// <summary>
/// Unknown error
/// </summary>
Unknown,
/// <summary>
/// Not authorized or insufficient permissions
/// </summary>
Unauthorized,
/// <summary>
/// Request rate limit error, too many requests
/// </summary>
RateLimitRequest,
/// <summary>
/// Connection rate limit error, too many connections
/// </summary>
RateLimitConnection,
/// <summary>
/// Subscription rate limit error, too many subscriptions
/// </summary>
RateLimitSubscription,
/// <summary>
/// Order rate limit error, too many orders
/// </summary>
RateLimitOrder,
/// <summary>
/// Request timestamp invalid
/// </summary>
InvalidTimestamp,
/// <summary>
/// Unknown symbol
/// </summary>
UnknownSymbol,
/// <summary>
/// Unknown asset
/// </summary>
UnknownAsset,
/// <summary>
/// Unknown order
/// </summary>
UnknownOrder,
/// <summary>
/// Duplicate subscription
/// </summary>
DuplicateSubscription,
/// <summary>
/// Invalid quantity
/// </summary>
InvalidQuantity,
/// <summary>
/// Invalid price
/// </summary>
InvalidPrice,
/// <summary>
/// Parameter(s) for stop or tp/sl order invalid
/// </summary>
InvalidStopParameters,
/// <summary>
/// Not enough balance to execute request
/// </summary>
InsufficientBalance,
/// <summary>
/// Client order id already in use
/// </summary>
DuplicateClientOrderId,
/// <summary>
/// Symbol is not currently trading
/// </summary>
UnavailableSymbol,
/// <summary>
/// Order rejected due to order configuration such as order type or time in force restrictions
/// </summary>
RejectedOrderConfiguration,
/// <summary>
/// There is no open position
/// </summary>
NoPosition,
/// <summary>
/// Max position reached
/// </summary>
MaxPosition,
/// <summary>
/// Error in the internal system
/// </summary>
SystemError,
/// <summary>
/// The target object is not in the correct state for an operation
/// </summary>
IncorrectState,
/// <summary>
/// Risk management error
/// </summary>
RiskError
#endregion
}
}
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using System; using System;
using System.Net;
using System.Net.Http;
namespace CryptoExchange.Net.Objects.Options namespace CryptoExchange.Net.Objects.Options
{ {
@@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// The HTTP protocol version to use, typically 2.0 or 1.1
/// </summary>
public Version HttpVersion { get; set; }
#if NET5_0_OR_GREATER
= new Version(2, 0);
#else
= new Version(1, 1);
#endif
/// <summary>
/// Http client keep alive interval for keeping connections open
/// </summary>
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
/// <summary> /// <summary>
/// Set the values of this options on the target options /// Set the values of this options on the target options
/// </summary> /// </summary>
@@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options
item.RateLimitingBehaviour = RateLimitingBehaviour; item.RateLimitingBehaviour = RateLimitingBehaviour;
item.CachingEnabled = CachingEnabled; item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge; item.CachingMaxAge = CachingMaxAge;
item.HttpVersion = HttpVersion;
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
return item; return item;
} }
} }
@@ -62,6 +62,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public bool PreventCaching { get; set; } public bool PreventCaching { get; set; }
/// <summary>
/// Whether the response to this requests should attempted to be parsed even when the status indicates failure
/// </summary>
public bool TryParseOnNonSuccess { get; set; }
/// <summary> /// <summary>
/// Connection id /// Connection id
/// </summary> /// </summary>
@@ -76,6 +81,9 @@ namespace CryptoExchange.Net.Objects
{ {
Path = path; Path = path;
Method = method; Method = method;
if (!Path.StartsWith("/"))
Path = $"/{Path}";
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -46,6 +46,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="parameterPosition">Parameter position</param> /// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param> /// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param> /// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
HttpMethod method, HttpMethod method,
@@ -57,8 +58,9 @@ namespace CryptoExchange.Net.Objects
RequestBodyFormat? requestBodyFormat = null, RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null,
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching); bool? tryParseOnNonSuccess = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
/// <summary> /// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache /// Get a definition if it is already in the cache or create a new definition and add it to the cache
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="parameterPosition">Parameter position</param> /// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param> /// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param> /// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
string identifier, string identifier,
@@ -86,7 +89,8 @@ namespace CryptoExchange.Net.Objects
RequestBodyFormat? requestBodyFormat = null, RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null, HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null, ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null) bool? preventCaching = null,
bool? tryParseOnNonSuccess = null)
{ {
if (!_definitions.TryGetValue(identifier, out var def)) if (!_definitions.TryGetValue(identifier, out var def))
@@ -100,7 +104,8 @@ namespace CryptoExchange.Net.Objects
ArraySerialization = arraySerialization, ArraySerialization = arraySerialization,
RequestBodyFormat = requestBodyFormat, RequestBodyFormat = requestBodyFormat,
ParameterPosition = parameterPosition, ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
}; };
_definitions.TryAdd(identifier, def); _definitions.TryAdd(identifier, def);
} }
@@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Rest request configuration
/// </summary>
public class RestRequestConfiguration
{
private string? _bodyContent;
private string? _queryString;
/// <summary>
/// Http method
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Whether the request needs authentication
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Base address for the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The request path
/// </summary>
public string Path { get; set; }
/// <summary>
/// Query parameters
/// </summary>
public IDictionary<string, object> QueryParameters { get; set; }
/// <summary>
/// Body parameters
/// </summary>
public IDictionary<string, object> BodyParameters { get; set; }
/// <summary>
/// Request headers
/// </summary>
public IDictionary<string, string> Headers { get; set; }
/// <summary>
/// Array serialization type
/// </summary>
public ArrayParametersSerialization ArraySerialization { get; set; }
/// <summary>
/// Position of the parameters
/// </summary>
public HttpMethodParameterPosition ParameterPosition { get; set; }
/// <summary>
/// Body format
/// </summary>
public RequestBodyFormat BodyFormat { get; set; }
/// <summary>
/// ctor
/// </summary>
public RestRequestConfiguration(
RequestDefinition requestDefinition,
string baseAddress,
IDictionary<string, object> queryParams,
IDictionary<string, object> bodyParams,
IDictionary<string, string> headers,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parametersPosition,
RequestBodyFormat bodyFormat)
{
Method = requestDefinition.Method;
Authenticated = requestDefinition.Authenticated;
Path = requestDefinition.Path;
BaseAddress = baseAddress;
QueryParameters = queryParams;
BodyParameters = bodyParams;
Headers = headers;
ArraySerialization = arraySerialization;
ParameterPosition = parametersPosition;
BodyFormat = bodyFormat;
}
/// <summary>
/// Get the parameter collection based on the ParameterPosition
/// </summary>
public IDictionary<string, object> GetPositionParameters()
{
if (ParameterPosition == HttpMethodParameterPosition.InBody)
return BodyParameters;
return QueryParameters;
}
/// <summary>
/// Get the query string. If it's not previously set it will return a newly formatted query string. If previously set return that.
/// </summary>
/// <param name="urlEncode">Whether to URL encode the parameter string if creating new</param>
public string GetQueryString(bool urlEncode = true)
{
return _queryString ?? QueryParameters.CreateParamString(urlEncode, ArraySerialization);
}
/// <summary>
/// Set the query string of the request. Will be returned by subsequent <see cref="GetQueryString" /> calls
/// </summary>
public void SetQueryString(string value)
{
_queryString = value;
}
/// <summary>
/// Get the body content if it's previously set
/// </summary>
public string? GetBodyContent()
{
return _bodyContent;
}
/// <summary>
/// Set the body content for the request
/// </summary>
public void SetBodyContent(string content)
{
_bodyContent = content;
}
}
}
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects.Sockets namespace CryptoExchange.Net.Objects.Sockets
@@ -12,13 +14,21 @@ namespace CryptoExchange.Net.Objects.Sockets
private readonly SocketConnection _connection; private readonly SocketConnection _connection;
private readonly Subscription _listener; private readonly Subscription _listener;
private object _eventLock = new object();
private List<Action> _connectionClosedEventHandlers = new List<Action>();
private List<Action> _connectionLostEventHandlers = new List<Action>();
private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>();
private List<Action<TimeSpan>> _connectionRestoredEventHandlers = new List<Action<TimeSpan>>();
private List<Action> _activityPausedEventHandlers = new List<Action>();
private List<Action> _activityUnpausedEventHandlers = new List<Action>();
/// <summary> /// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible. /// Event when the connection is lost. The socket will automatically reconnect when possible.
/// </summary> /// </summary>
public event Action ConnectionLost public event Action ConnectionLost
{ {
add => _connection.ConnectionLost += value; add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
remove => _connection.ConnectionLost -= value; remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
} }
/// <summary> /// <summary>
@@ -26,8 +36,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public event Action ConnectionClosed public event Action ConnectionClosed
{ {
add => _connection.ConnectionClosed += value; add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
remove => _connection.ConnectionClosed -= value; remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
} }
/// <summary> /// <summary>
@@ -35,8 +45,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public event Action<Error> ResubscribingFailed public event Action<Error> ResubscribingFailed
{ {
add => _connection.ResubscribingFailed += value; add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
remove => _connection.ResubscribingFailed -= value; remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
} }
/// <summary> /// <summary>
@@ -46,8 +56,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public event Action<TimeSpan> ConnectionRestored public event Action<TimeSpan> ConnectionRestored
{ {
add => _connection.ConnectionRestored += value; add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
remove => _connection.ConnectionRestored -= value; remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
} }
/// <summary> /// <summary>
@@ -55,8 +65,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public event Action ActivityPaused public event Action ActivityPaused
{ {
add => _connection.ActivityPaused += value; add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
remove => _connection.ActivityPaused -= value; remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
} }
/// <summary> /// <summary>
@@ -64,8 +74,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public event Action ActivityUnpaused public event Action ActivityUnpaused
{ {
add => _connection.ActivityUnpaused += value; add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
remove => _connection.ActivityUnpaused -= value; remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
} }
/// <summary> /// <summary>
@@ -95,7 +105,85 @@ namespace CryptoExchange.Net.Objects.Sockets
public UpdateSubscription(SocketConnection connection, Subscription subscription) public UpdateSubscription(SocketConnection connection, Subscription subscription)
{ {
_connection = connection; _connection = connection;
_connection.ConnectionClosed += HandleConnectionClosedEvent;
_connection.ConnectionLost += HandleConnectionLostEvent;
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
_connection.ActivityPaused += HandlePausedEvent;
_connection.ActivityUnpaused += HandleUnpausedEvent;
_listener = subscription; _listener = subscription;
_listener.Unsubscribed += HandleUnsubscribed;
}
private void HandleUnsubscribed()
{
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
}
private void HandleConnectionClosedEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _connectionClosedEventHandlers.ToList();
foreach(var callback in handlers)
callback();
}
private void HandleConnectionLostEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _connectionLostEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
{
List<Action<TimeSpan>> handlers;
lock (_eventLock)
handlers = _connectionRestoredEventHandlers.ToList();
foreach (var callback in handlers)
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
{
List<Action<Error>> handlers;
lock (_eventLock)
handlers = _resubscribeFailedEventHandlers.ToList();
foreach (var callback in handlers)
callback(error);
}
private void HandlePausedEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _activityPausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleUnpausedEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _activityUnpausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
} }
/// <summary> /// <summary>
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -549,7 +550,7 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(new CancellationRequestedError()); return new CallResult<bool>(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > timeout) if (DateTime.UtcNow - startWait > timeout)
return new CallResult<bool>(new ServerError("Timeout while waiting for data")); return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try try
{ {
+6 -1
View File
@@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc /> /// <inheritdoc />
public Uri Uri => _request.RequestUri!; public Uri Uri => _request.RequestUri!;
/// <inheritdoc />
public Version HttpVersion => _request.Version!;
/// <inheritdoc /> /// <inheritdoc />
public int RequestId { get; } public int RequestId { get; }
@@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc /> /// <inheritdoc />
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken) public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
{ {
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
return new Response(response);
} }
} }
} }
+16 -26
View File
@@ -3,6 +3,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Requests namespace CryptoExchange.Net.Requests
{ {
@@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests
private HttpClient? _httpClient; private HttpClient? _httpClient;
/// <inheritdoc /> /// <inheritdoc />
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null) public void Configure(RestExchangeOptions options, HttpClient? client = null)
{ {
if (client == null) if (client == null)
client = CreateClient(proxy, requestTimeout); client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
_httpClient = client; _httpClient = client;
} }
/// <inheritdoc /> /// <inheritdoc />
public IRequest Create(HttpMethod method, Uri uri, int requestId) public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
{ {
if (_httpClient == null) if (_httpClient == null)
throw new InvalidOperationException("Cant create request before configuring http client"); throw new InvalidOperationException("Cant create request before configuring http client");
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId); var requestMessage = new HttpRequestMessage(method, uri);
requestMessage.Version = httpRequestVersion;
#if NET5_0_OR_GREATER
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
#endif
return new Request(requestMessage, _httpClient, requestId);
} }
/// <inheritdoc /> /// <inheritdoc />
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{ {
_httpClient = CreateClient(proxy, requestTimeout); _httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
} }
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout) private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
{ {
var handler = new HttpClientHandler(); var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
try
{
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
}
catch (PlatformNotSupportedException) { }
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
if (proxy != null)
{
handler.Proxy = new WebProxy
{
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
};
}
var client = new HttpClient(handler) var client = new HttpClient(handler)
{ {
Timeout = requestTimeout Timeout = requestTimeout
}; };
return client; return client;
} }
} }
} }
+5 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc /> /// <inheritdoc />
public HttpStatusCode StatusCode => _response.StatusCode; public HttpStatusCode StatusCode => _response.StatusCode;
/// <inheritdoc />
public Version HttpVersion => _response.Version;
/// <inheritdoc /> /// <inheritdoc />
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode; public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
@@ -20,6 +20,10 @@
/// <summary> /// <summary>
/// Quantity can be either base or quote quantity /// Quantity can be either base or quote quantity
/// </summary> /// </summary>
BaseAndQuoteAsset BaseAndQuoteAsset,
/// <summary>
/// Quantity can be either base or quote quantity, or in contracts
/// </summary>
BaseAndQuoteAssetAndContracts
} }
} }
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis
WebCallResult<T> result, WebCallResult<T> result,
INextPageToken? nextPageToken = null) : INextPageToken? nextPageToken = null) :
base(result.ResponseStatusCode, base(result.ResponseStatusCode,
result.HttpVersion,
result.ResponseHeaders, result.ResponseHeaders,
result.ResponseTime, result.ResponseTime,
result.ResponseLength, result.ResponseLength,
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis
WebCallResult<T> result, WebCallResult<T> result,
INextPageToken? nextPageToken = null) : INextPageToken? nextPageToken = null) :
base(result.ResponseStatusCode, base(result.ResponseStatusCode,
result.HttpVersion,
result.ResponseHeaders, result.ResponseHeaders,
result.ResponseTime, result.ResponseTime,
result.ResponseLength, result.ResponseLength,
@@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis
string exchange, string exchange,
TradingMode[]? dataTradeModes, TradingMode[]? dataTradeModes,
HttpStatusCode? code, HttpStatusCode? code,
Version? httpVersion,
KeyValuePair<string, string[]>[]? responseHeaders, KeyValuePair<string, string[]>[]? responseHeaders,
TimeSpan? responseTime, TimeSpan? responseTime,
long? responseLength, long? responseLength,
@@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis
Error? error, Error? error,
INextPageToken? nextPageToken = null) : base( INextPageToken? nextPageToken = null) : base(
code, code,
httpVersion,
responseHeaders, responseHeaders,
responseTime, responseTime,
responseLength, responseLength,
@@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
/// <returns></returns> /// <returns></returns>
public new ExchangeWebResult<K> As<K>([AllowNull] K data) public new ExchangeWebResult<K> As<K>([AllowNull] K data)
{ {
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken); return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -58,19 +58,19 @@ namespace CryptoExchange.Net.SharedApis
public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes) public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
{ {
if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value)) if (tradingMode != null && !supportedTradingModes.Contains(tradingMode.Value))
return new ArgumentError($"ApiType.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}"); return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
foreach (var param in RequiredExchangeParameters) foreach (var param in RequiredExchangeParameters)
{ {
if (!string.IsNullOrEmpty(param.Name)) if (!string.IsNullOrEmpty(param.Name))
{ {
if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true) if (ExchangeParameters.HasValue(exchangeParameters, exchange, param.Name!, param.ValueType) != true)
return new ArgumentError($"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}"); return ArgumentError.Invalid(param.Name!, $"Required exchange parameter `{param.Name}` for exchange `{exchange}` is missing or has incorrect type. Expected type is {param.ValueType.Name}. Example: {param.ExampleValue}");
} }
else else
{ {
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true)) if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, exchange, x, param.ValueType) != true))
return new ArgumentError($"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
} }
} }
@@ -109,6 +109,15 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public List<ParameterDescription> RequiredOptionalParameters { get; set; } = new List<ParameterDescription>(); public List<ParameterDescription> RequiredOptionalParameters { get; set; } = new List<ParameterDescription>();
/// <summary>
/// Whether this accepts multiple symbols (Only applicable to request requiring symbol parameters)
/// </summary>
public bool SupportsMultipleSymbols { get; set; } = false;
/// <summary>
/// The max number of symbols which can be passed in a call (Only applicable to request requiring symbol parameters)
/// </summary>
public int? MaxSymbolCount { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -131,12 +140,25 @@ namespace CryptoExchange.Net.SharedApis
if (!string.IsNullOrEmpty(param.Name)) if (!string.IsNullOrEmpty(param.Name))
{ {
if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null) if (typeof(T).GetProperty(param.Name)!.GetValue(request, null) == null)
return new ArgumentError($"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}"); return ArgumentError.Invalid(param.Name!, $"Required optional parameter `{param.Name}` for exchange `{exchange}` is missing. Example: {param.ExampleValue}");
} }
else else
{ {
if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null)) if (param.Names!.All(x => typeof(T).GetProperty(param.Name!)!.GetValue(request, null) == null))
return new ArgumentError($"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}"); return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{exchange}` should be provided. Example: {param.ExampleValue}");
}
}
if (request is SharedSymbolRequest symbolsRequest)
{
if (symbolsRequest.Symbols != null)
{
if (!SupportsMultipleSymbols)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported");
if (symbolsRequest.Symbols.Length > MaxSymbolCount)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed");
} }
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (TimeFilterSupported && request.StartTime != null) if (TimeFilterSupported && request.StartTime != null)
return new ArgumentError($"Time filter is not supported"); return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (TimeFilterSupported && request.StartTime != null) if (TimeFilterSupported && request.StartTime != null)
return new ArgumentError($"Time filter is not supported"); return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -67,23 +67,23 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (!IsSupported(request.Interval)) if (!IsSupported(request.Interval))
return new ArgumentError("Interval not supported"); return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported");
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} klines are available"); return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
if (request.Limit > MaxLimit) if (request.Limit > MaxLimit)
return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request"); return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request");
if (MaxTotalDataPoints.HasValue) if (MaxTotalDataPoints.HasValue)
{ {
if (request.Limit > MaxTotalDataPoints.Value) if (request.Limit > MaxTotalDataPoints.Value)
return new ArgumentError($"Only the most recent {MaxTotalDataPoints} klines are available"); return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only the most recent {MaxTotalDataPoints} klines are available");
if (request.StartTime.HasValue == true) if (request.StartTime.HasValue == true)
{ {
if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value) if (((request.EndTime ?? DateTime.UtcNow) - request.StartTime.Value).TotalSeconds / (int)request.Interval > MaxTotalDataPoints.Value)
return new ArgumentError($"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed"); return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxTotalDataPoints} klines are available, time filter failed");
} }
} }
@@ -49,13 +49,13 @@ namespace CryptoExchange.Net.SharedApis
return null; return null;
if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) if (MaxLimit.HasValue && request.Limit.Value > MaxLimit)
return new ArgumentError($"Max limit is {MaxLimit}"); return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}");
if (MinLimit.HasValue && request.Limit.Value < MinLimit) if (MinLimit.HasValue && request.Limit.Value < MinLimit)
return new ArgumentError($"Min limit is {MaxLimit}"); return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Min limit is {MaxLimit}");
if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value)) if (SupportedLimits != null && !SupportedLimits.Contains(request.Limit.Value))
return new ArgumentError($"Limit should be one of " + string.Join(", ", SupportedLimits)); return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Limit should be one of " + string.Join(", ", SupportedLimits));
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public Error? Validate(GetRecentTradesRequest request) public Error? Validate(GetRecentTradesRequest request)
{ {
if (request.Limit > MaxLimit) if (request.Limit > MaxLimit)
return new ArgumentError($"Only the most recent {MaxLimit} trades are available"); return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available");
return null; return null;
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value)) if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
return new ArgumentError($"Only the most recent {MaxAge} trades are available"); return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (TimeFilterSupported && request.StartTime != null) if (TimeFilterSupported && request.StartTime != null)
return new ArgumentError($"Time filter is not supported"); return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -36,16 +36,16 @@ namespace CryptoExchange.Net.SharedApis
SharedQuantitySupport quantitySupport) SharedQuantitySupport quantitySupport)
{ {
if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null))
return new ArgumentError("Tp/Sl parameters not supported"); return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.StopLossPrice) + " / " + nameof(PlaceFuturesOrderRequest.TakeProfitPrice), "Tp/Sl parameters not supported");
if (request.OrderType == SharedOrderType.Other) if (request.OrderType == SharedOrderType.Other)
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
if (!supportedOrderTypes.Contains(request.OrderType)) if (!supportedOrderTypes.Contains(request.OrderType))
return new ArgumentError("Order type not supported"); return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.OrderType), "Order type not supported");
if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value))
return new ArgumentError("Order time in force not supported"); return ArgumentError.Invalid(nameof(PlaceFuturesOrderRequest.TimeInForce), "Order time in force not supported");
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
if (quantityError != null) if (quantityError != null)
@@ -34,10 +34,10 @@ namespace CryptoExchange.Net.SharedApis
throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType));
if (!supportedOrderTypes.Contains(request.OrderType)) if (!supportedOrderTypes.Contains(request.OrderType))
return new ArgumentError("Order type not supported"); return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.OrderType), "Order type not supported");
if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value))
return new ArgumentError("Order time in force not supported"); return ArgumentError.Invalid(nameof(PlaceSpotOrderRequest.TimeInForce), "Order time in force not supported");
var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity);
if (quantityError != null) if (quantityError != null)
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (!IsSupported(request.Interval)) if (!IsSupported(request.Interval))
return new ArgumentError("Interval not supported"); return ArgumentError.Invalid(nameof(SubscribeKlineRequest.Interval), "Interval not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value)) if (request.Limit != null && !SupportedLimits.Contains(request.Limit.Value))
return new ArgumentError("Limit not supported"); return ArgumentError.Invalid(nameof(SubscribeOrderBookRequest.Limit), "Limit not supported");
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
} }
@@ -65,6 +65,9 @@ namespace CryptoExchange.Net.SharedApis
if (supportedType == quantityType) if (supportedType == quantityType)
return true; return true;
if (supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
return true;
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset)) if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset))
return true; return true;
@@ -77,20 +80,20 @@ namespace CryptoExchange.Net.SharedApis
public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity) public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity)
{ {
var supportedType = GetSupportedQuantityType(side, type); var supportedType = GetSupportedQuantityType(side, type);
if (supportedType == SharedQuantityType.BaseAndQuoteAsset) if (supportedType == SharedQuantityType.BaseAndQuoteAsset || supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
return null; return null;
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null) if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null)
return new ArgumentError($"Quantity for {side}.{type} required in base or quote asset"); return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base or quote asset");
if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null) if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null)
return new ArgumentError($"Quantity for {side}.{type} required in quote asset"); return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in quote asset");
if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null) if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null)
return new ArgumentError($"Quantity for {side}.{type} required in base asset"); return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in base asset");
if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null) if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null)
return new ArgumentError($"Quantity for {side}.{type} required in contracts"); return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in contracts");
return null; return null;
} }
@@ -1,14 +1,27 @@
namespace CryptoExchange.Net.SharedApis using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Symbol request /// Symbol request
/// </summary> /// </summary>
public record SharedSymbolRequest : SharedRequest public record SharedSymbolRequest : SharedRequest
{ {
/// <summary>
/// Trading mode
/// </summary>
public TradingMode TradingMode { get; }
/// <summary> /// <summary>
/// The symbol /// The symbol
/// </summary> /// </summary>
public SharedSymbol Symbol { get; set; } public SharedSymbol? Symbol { get; set; }
/// <summary>
/// Symbols
/// </summary>
public SharedSymbol[]? Symbols { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
@@ -16,6 +29,22 @@
public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters) public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
{ {
Symbol = symbol; Symbol = symbol;
TradingMode = symbol.TradingMode;
}
/// <summary>
/// ctor
/// </summary>
public SharedSymbolRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
{
if (!symbols.Any())
throw new ArgumentException("Empty symbol list");
if (symbols.GroupBy(x => x.TradingMode).Count() > 1)
throw new ArgumentException("All symbols in the symbol list should have the same trading mode");
Symbols = symbols.ToArray();
TradingMode = Symbols.First().TradingMode;
} }
} }
} }
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Request to subscribe to book ticker updates /// Request to subscribe to book ticker updates
@@ -13,5 +15,22 @@
public SubscribeBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) public SubscribeBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{ {
} }
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeBookTickerRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeBookTickerRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
} }
} }
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Request to subscribe to kline/candlestick updates /// Request to subscribe to kline/candlestick updates
@@ -20,5 +22,26 @@
{ {
Interval = interval; Interval = interval;
} }
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="interval">Kline interval</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeKlineRequest(IEnumerable<SharedSymbol> symbols, SharedKlineInterval interval, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
Interval = interval;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="interval">Kline interval</param>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeKlineRequest(SharedKlineInterval interval, params SharedSymbol[] symbols) : base(symbols, null)
{
Interval = interval;
}
} }
} }
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Request to subscribe to order book snapshot updates /// Request to subscribe to order book snapshot updates
@@ -20,5 +22,22 @@
{ {
Limit = limit; Limit = limit;
} }
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeOrderBookRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeOrderBookRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
} }
} }
@@ -1,4 +1,7 @@
namespace CryptoExchange.Net.SharedApis using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Request to subscribe to ticker updates /// Request to subscribe to ticker updates
@@ -13,5 +16,22 @@
public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) public SubscribeTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{ {
} }
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeTickerRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeTickerRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
} }
} }
@@ -1,4 +1,6 @@
namespace CryptoExchange.Net.SharedApis using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Request to subscribe to trade updates /// Request to subscribe to trade updates
@@ -13,5 +15,22 @@
public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) public SubscribeTradeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
{ {
} }
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeTradeRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null) : base(symbols, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
public SubscribeTradeRequest(params SharedSymbol[] symbols) : base(symbols, null)
{
}
} }
} }
@@ -67,6 +67,10 @@ namespace CryptoExchange.Net.SharedApis
/// Min number of confirmations /// Min number of confirmations
/// </summary> /// </summary>
public int? MinConfirmations { get; set; } public int? MinConfirmations { get; set; }
/// <summary>
/// The contract address
/// </summary>
public string? ContractAddress { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
@@ -30,5 +30,8 @@ namespace CryptoExchange.Net.SharedApis
public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType) public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType)
{ {
} }
/// <inheritdoc />
public override SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant(), DeliveryTime) { SymbolName = Name };
} }
} }
@@ -7,6 +7,10 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public record SharedPosition : SharedSymbolModel public record SharedPosition : SharedSymbolModel
{ {
/// <summary>
/// Position id
/// </summary>
public string? Id { get; set; }
/// <summary> /// <summary>
/// Current size of the position /// Current size of the position
/// </summary> /// </summary>
@@ -69,5 +69,11 @@
Name = symbol; Name = symbol;
Trading = trading; Trading = trading;
} }
/// <summary>
/// The SharedSymbol of this symbol
/// </summary>
/// <returns></returns>
public virtual SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant()) { SymbolName = Name };
} }
} }
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -252,6 +253,11 @@ namespace CryptoExchange.Net.Sockets
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return new CallResult(new ServerRateLimitError(we.Message, we)); return new CallResult(new ServerRateLimitError(we.Message, we));
} }
if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized)
{
return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected")));
}
#else #else
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
// Try to read 429 from the message instead // Try to read 429 from the message instead
@@ -0,0 +1,180 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Message link type
/// </summary>
public enum MessageLinkType
{
/// <summary>
/// Match when the listen id matches fully to the value
/// </summary>
Full,
/// <summary>
/// Match when the listen id starts with the value
/// </summary>
StartsWith
}
/// <summary>
/// Matches a message listen id to a specific listener
/// </summary>
public class MessageMatcher
{
/// <summary>
/// Linkers in this matcher
/// </summary>
public MessageHandlerLink[] HandlerLinks { get; }
/// <summary>
/// ctor
/// </summary>
private MessageMatcher(params MessageHandlerLink[] links)
{
HandlerLinks = links;
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(string value)
{
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, (con, msg) => new CallResult<T>(default, msg.OriginalData, null)));
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(string value, Func<SocketConnection, DataEvent<T>, CallResult> handler)
{
return new MessageMatcher(new MessageHandlerLink<T>(MessageLinkType.Full, value, handler));
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(IEnumerable<string> values, Func<SocketConnection, DataEvent<T>, CallResult> handler)
{
return new MessageMatcher(values.Select(x => new MessageHandlerLink<T>(MessageLinkType.Full, x, handler)).ToArray());
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create<T>(MessageLinkType type, string value, Func<SocketConnection, DataEvent<T>, CallResult> handler)
{
return new MessageMatcher(new MessageHandlerLink<T>(type, value, handler));
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageMatcher Create(params MessageHandlerLink[] linkers)
{
return new MessageMatcher(linkers);
}
/// <summary>
/// Whether this matcher contains a specific link
/// </summary>
public bool ContainsCheck(MessageHandlerLink link) => HandlerLinks.Any(x => x.Type == link.Type && x.Value == link.Value);
/// <summary>
/// Get any handler links matching with the listen id
/// </summary>
public List<MessageHandlerLink> GetHandlerLinks(string listenId) => HandlerLinks.Where(x => x.Check(listenId)).ToList();
/// <inheritdoc />
public override string ToString() => string.Join(",", HandlerLinks.Select(x => x.ToString()));
}
/// <summary>
/// Message handler link
/// </summary>
public abstract class MessageHandlerLink
{
/// <summary>
/// Type of check
/// </summary>
public MessageLinkType Type { get; }
/// <summary>
/// String value of the check
/// </summary>
public string Value { get; }
/// <summary>
/// Deserialization type
/// </summary>
public abstract Type GetDeserializationType(IMessageAccessor accessor);
/// <summary>
/// ctor
/// </summary>
public MessageHandlerLink(MessageLinkType type, string value)
{
Type = type;
Value = value;
}
/// <summary>
/// Whether this listen id matches this link
/// </summary>
public bool Check(string listenId)
{
if (Type == MessageLinkType.Full)
return Value.Equals(listenId, StringComparison.Ordinal);
return listenId.StartsWith(Value, StringComparison.Ordinal);
}
/// <summary>
/// Message handler
/// </summary>
public abstract CallResult Handle(SocketConnection connection, DataEvent<object> message);
/// <inheritdoc />
public override string ToString() => $"{Type} match for \"{Value}\"";
}
/// <summary>
/// Message handler link
/// </summary>
public class MessageHandlerLink<TServer>: MessageHandlerLink
{
private Func<SocketConnection, DataEvent<TServer>, CallResult> _handler;
/// <inheritdoc />
public override Type GetDeserializationType(IMessageAccessor accessor) => typeof(TServer);
/// <summary>
/// ctor
/// </summary>
public MessageHandlerLink(string value, Func<SocketConnection, DataEvent<TServer>, CallResult> handler)
: this(MessageLinkType.Full, value, handler)
{
}
/// <summary>
/// ctor
/// </summary>
public MessageHandlerLink(MessageLinkType type, string value, Func<SocketConnection, DataEvent<TServer>, CallResult> handler)
: base(type, value)
{
_handler = handler;
}
/// <inheritdoc />
public override CallResult Handle(SocketConnection connection, DataEvent<object> message)
{
return _handler(connection, message.As((TServer)message.Data));
}
}
}
+22 -64
View File
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -28,6 +29,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public TimeSpan? RequestTimeout { get; set; } public TimeSpan? RequestTimeout { get; set; }
/// <summary>
/// What should happen if the query times out
/// </summary>
public TimeoutBehavior TimeoutBehavior { get; set; } = TimeoutBehavior.Fail;
/// <summary> /// <summary>
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request, /// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
/// and each symbol receives it's own confirmation response /// and each symbol receives it's own confirmation response
@@ -60,9 +66,9 @@ namespace CryptoExchange.Net.Sockets
public AsyncResetEvent? ContinueAwaiter { get; set; } public AsyncResetEvent? ContinueAwaiter { get; set; }
/// <summary> /// <summary>
/// Strings to match this query to a received message /// Matcher for this query
/// </summary> /// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; } public MessageMatcher MessageMatcher { get; set; } = null!;
/// <summary> /// <summary>
/// The query request object /// The query request object
@@ -84,13 +90,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool ExpectsResponse { get; set; } = true; public bool ExpectsResponse { get; set; } = true;
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary> /// <summary>
/// Wait event for response /// Wait event for response
/// </summary> /// </summary>
@@ -161,23 +160,16 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Handle a response message /// Handle a response message
/// </summary> /// </summary>
/// <param name="message"></param> public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check);
/// <param name="connection"></param>
/// <returns></returns>
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
} }
/// <summary> /// <summary>
/// Query /// Query
/// </summary> /// </summary>
/// <typeparam name="TServerResponse">The type returned from the server</typeparam>
/// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam> /// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
public abstract class Query<TServerResponse, THandlerResponse> : Query public abstract class Query<THandlerResponse> : Query
{ {
/// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
/// <summary> /// <summary>
/// The typed call result /// The typed call result
/// </summary> /// </summary>
@@ -194,25 +186,22 @@ namespace CryptoExchange.Net.Sockets
} }
/// <inheritdoc /> /// <inheritdoc />
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message) public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check)
{ {
var typedMessage = message.As((TServerResponse)message.Data); if (!PreCheckMessage(connection, message))
if (!ValidateMessage(typedMessage))
return CallResult.SuccessResult; return CallResult.SuccessResult;
CurrentResponses++; CurrentResponses++;
if (CurrentResponses == RequiredResponses) if (CurrentResponses == RequiredResponses)
{
Completed = true;
Response = message.Data; Response = message.Data;
}
if (Result?.Success != false) if (Result?.Success != false)
// If an error result is already set don't override that // If an error result is already set don't override that
Result = HandleMessage(connection, typedMessage); Result = check.Handle(connection, message);
if (CurrentResponses == RequiredResponses) if (CurrentResponses == RequiredResponses)
{ {
Completed = true;
_event.Set(); _event.Set();
if (ContinueAwaiter != null) if (ContinueAwaiter != null)
await ContinueAwaiter.WaitAsync().ConfigureAwait(false); await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
@@ -224,26 +213,20 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Validate if a message is actually processable by this query /// Validate if a message is actually processable by this query
/// </summary> /// </summary>
/// <param name="message"></param> public virtual bool PreCheckMessage(SocketConnection connection, DataEvent<object> message) => true;
/// <returns></returns>
public virtual bool ValidateMessage(DataEvent<TServerResponse> message) => true;
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult<THandlerResponse> HandleMessage(SocketConnection connection, DataEvent<TServerResponse> message);
/// <inheritdoc /> /// <inheritdoc />
public override void Timeout() public override void Timeout()
{ {
if (Completed) if (Completed)
return; return;
Completed = true; Completed = true;
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null)); if (TimeoutBehavior == TimeoutBehavior.Fail)
Result = new CallResult<THandlerResponse>(new TimeoutError());
else
Result = new CallResult<THandlerResponse>(default, null, default);
ContinueAwaiter?.Set(); ContinueAwaiter?.Set();
_event.Set(); _event.Set();
} }
@@ -257,29 +240,4 @@ namespace CryptoExchange.Net.Sockets
_event.Set(); _event.Set();
} }
} }
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query<TResponse, TResponse>
{
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
}
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public override CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => message.ToCallResult();
}
} }
+101 -83
View File
@@ -11,8 +11,6 @@ using System.Diagnostics;
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using System.Threading; using System.Threading;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
@@ -204,6 +202,18 @@ namespace CryptoExchange.Net.Sockets
} }
} }
/// <summary>
/// The number of current pending requests
/// </summary>
public int PendingRequests
{
get
{
lock (_listenersLock)
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
}
}
private bool _pausedActivity; private bool _pausedActivity;
private readonly object _listenersLock; private readonly object _listenersLock;
private readonly List<IMessageProcessor> _listeners; private readonly List<IMessageProcessor> _listeners;
@@ -229,6 +239,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
private readonly IWebsocket _socket; private readonly IWebsocket _socket;
/// <summary>
/// Cache for deserialization, only caches for a single message
/// </summary>
private readonly Dictionary<Type, object> _deserializationCache = new Dictionary<Type, object>();
/// <summary> /// <summary>
/// New socket connection /// New socket connection
/// </summary> /// </summary>
@@ -446,9 +461,6 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Handle a message /// Handle a message
/// </summary> /// </summary>
/// <param name="data"></param>
/// <param name="type"></param>
/// <returns></returns>
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
@@ -475,9 +487,9 @@ namespace CryptoExchange.Net.Sockets
_logger.ReceivedData(SocketId, originalData); _logger.ReceivedData(SocketId, originalData);
} }
if (!accessor.IsValid) if (!accessor.IsValid && !ApiClient.ProcessUnparsableMessages)
{ {
_logger.FailedToParse(SocketId, result.Error!.Message); _logger.FailedToParse(SocketId, result.Error!.Message ?? result.Error!.ErrorDescription!);
return; return;
} }
@@ -485,7 +497,7 @@ namespace CryptoExchange.Net.Sockets
var listenId = ApiClient.GetListenerIdentifier(accessor); var listenId = ApiClient.GetListenerIdentifier(accessor);
if (listenId == null) if (listenId == null)
{ {
originalData = outputOriginalData ? accessor.GetOriginalString() : "[OutputOriginalData is false]"; originalData ??= "[OutputOriginalData is false]";
if (!ApiClient.UnhandledMessageExpected) if (!ApiClient.UnhandledMessageExpected)
_logger.FailedToEvaluateMessage(SocketId, originalData); _logger.FailedToEvaluateMessage(SocketId, originalData);
@@ -493,18 +505,81 @@ namespace CryptoExchange.Net.Sockets
return; return;
} }
// 4. Get the listeners interested in this message bool processed = false;
List<IMessageProcessor> processors; var totalUserTime = 0;
lock (_listenersLock)
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
if (processors.Count == 0) List<IMessageProcessor> localListeners;
lock(_listenersLock)
localListeners = _listeners.ToList();
foreach(var processor in localListeners)
{
foreach(var listener in processor.MessageMatcher.GetHandlerLinks(listenId))
{
processed = true;
_logger.ProcessorMatched(SocketId, listener.ToString(), listenId);
// 4. Determine the type to deserialize to for this processor
var messageType = listener.GetDeserializationType(accessor);
if (messageType == null)
{
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
continue;
}
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
{
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
subscriptionProcessor.Confirmed = true;
if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed)
// If this subscription has a query waiting for a timeout (success if there is no error response)
// then time it out now as the data is being received, so we assume it's successful
subscriptionProcessor.SubscriptionQuery.Timeout();
}
// 5. Deserialize the message
_deserializationCache.TryGetValue(messageType, out var deserialized);
if (deserialized == null)
{
var desResult = processor.Deserialize(accessor, messageType);
if (!desResult)
{
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
continue;
}
deserialized = desResult.Data;
_deserializationCache.Add(messageType, deserialized);
}
// 6. Pass the message to the handler
try
{
var innerSw = Stopwatch.StartNew();
await processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null), listener).ConfigureAwait(false);
if (processor is Query query && query.RequiredResponses != 1)
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
totalUserTime += (int)innerSw.ElapsedMilliseconds;
}
catch (Exception ex)
{
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
if (processor is Subscription subscription)
subscription.InvokeExceptionHandler(ex);
}
}
}
if (!processed)
{ {
if (!ApiClient.UnhandledMessageExpected) if (!ApiClient.UnhandledMessageExpected)
{ {
List<string> listenerIds; List<string> listenerIds;
lock (_listenersLock) lock (_listenersLock)
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList(); listenerIds = _listeners.Select(l => l.MessageMatcher.ToString()).ToList();
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds)); _logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
UnhandledMessage?.Invoke(accessor); UnhandledMessage?.Invoke(accessor);
} }
@@ -512,69 +587,11 @@ namespace CryptoExchange.Net.Sockets
return; return;
} }
_logger.ProcessorMatched(SocketId, processors.Count, listenId);
var totalUserTime = 0;
Dictionary<Type, object>? desCache = null;
if (processors.Count > 1)
{
// Only instantiate a cache if there are multiple processors
desCache = new Dictionary<Type, object>();
}
foreach (var processor in processors)
{
// 5. Determine the type to deserialize to for this processor
var messageType = processor.GetMessageType(accessor);
if (messageType == null)
{
_logger.ReceivedMessageNotRecognized(SocketId, processor.Id);
continue;
}
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
{
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
subscriptionProcessor.Confirmed = true;
// This doesn't trigger a waiting subscribe query, should probably also somehow set the wait event for that
}
// 6. Deserialize the message
object? deserialized = null;
desCache?.TryGetValue(messageType, out deserialized);
if (deserialized == null)
{
var desResult = processor.Deserialize(accessor, messageType);
if (!desResult)
{
_logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception);
continue;
}
deserialized = desResult.Data;
desCache?.Add(messageType, deserialized);
}
// 7. Hand of the message to the subscription
try
{
var innerSw = Stopwatch.StartNew();
await processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null)).ConfigureAwait(false);
if (processor is Query query && query.RequiredResponses != 1)
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
totalUserTime += (int)innerSw.ElapsedMilliseconds;
}
catch (Exception ex)
{
_logger.UserMessageProcessingFailed(SocketId, ex.Message, ex);
if (processor is Subscription subscription)
subscription.InvokeExceptionHandler(ex);
}
}
_logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime); _logger.MessageProcessed(SocketId, sw.ElapsedMilliseconds, sw.ElapsedMilliseconds - totalUserTime);
} }
finally finally
{ {
_deserializationCache.Clear();
accessor.Clear(); accessor.Clear();
} }
} }
@@ -654,7 +671,7 @@ namespace CryptoExchange.Net.Sockets
bool anyDuplicateSubscription; bool anyDuplicateSubscription;
lock (_listenersLock) lock (_listenersLock)
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l))); anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageMatcher.HandlerLinks.All(l => subscription.MessageMatcher.ContainsCheck(l)));
bool shouldCloseConnection; bool shouldCloseConnection;
lock (_listenersLock) lock (_listenersLock)
@@ -689,6 +706,8 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock) lock (_listenersLock)
_listeners.Remove(subscription); _listeners.Remove(subscription);
subscription.InvokeUnsubscribedHandler();
} }
/// <summary> /// <summary>
@@ -763,22 +782,21 @@ namespace CryptoExchange.Net.Sockets
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
{ {
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.Result ?? new CallResult(new ServerError("Timeout")); return query.Result ?? new CallResult(new TimeoutError());
} }
/// <summary> /// <summary>
/// Send a query request and wait for an answer /// Send a query request and wait for an answer
/// </summary> /// </summary>
/// <typeparam name="THandlerResponse">Expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">Query to send</param> /// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param> /// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default) public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<THandlerResponse>(Query<THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
{ {
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout")); return query.TypedResult ?? new CallResult<THandlerResponse>(new TimeoutError());
} }
private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default) private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default)
@@ -863,7 +881,7 @@ namespace CryptoExchange.Net.Sockets
{ {
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{ {
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info)); return new CallResult(new InvalidOperationError(info));
} }
@@ -898,7 +916,7 @@ namespace CryptoExchange.Net.Sockets
{ {
if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value)
{ {
var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; var info = $"Message to send exceeds the max server message size ({data.Length} vs {ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit";
_logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info); _logger.LogWarning("[Sckt {SocketId}] [Req {RequestId}] {Info}", SocketId, requestId, info);
return new CallResult(new InvalidOperationError(info)); return new CallResult(new InvalidOperationError(info));
} }
@@ -995,7 +1013,7 @@ namespace CryptoExchange.Net.Sockets
return result; return result;
} }
var subQuery = subscription.GetSubQuery(this); var subQuery = subscription.CreateSubscriptionQuery(this);
if (subQuery == null) if (subQuery == null)
{ {
subscription.IsResubscribing = false; subscription.IsResubscribing = false;
@@ -1030,7 +1048,7 @@ namespace CryptoExchange.Net.Sockets
internal async Task UnsubscribeAsync(Subscription subscription) internal async Task UnsubscribeAsync(Subscription subscription)
{ {
var unsubscribeRequest = subscription.GetUnsubQuery(); var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
if (unsubscribeRequest == null) if (unsubscribeRequest == null)
return; return;
@@ -1043,7 +1061,7 @@ namespace CryptoExchange.Net.Sockets
if (!_socket.IsOpen) if (!_socket.IsOpen)
return new CallResult(new WebError("Socket is not connected")); return new CallResult(new WebError("Socket is not connected"));
var subQuery = subscription.GetSubQuery(this); var subQuery = subscription.CreateSubscriptionQuery(this);
if (subQuery == null) if (subQuery == null)
return CallResult.SuccessResult; return CallResult.SuccessResult;
+50 -28
View File
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -60,9 +61,9 @@ namespace CryptoExchange.Net.Sockets
public bool Authenticated { get; } public bool Authenticated { get; }
/// <summary> /// <summary>
/// Strings to match this subscription to a received message /// Matcher for this subscription
/// </summary> /// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; } public MessageMatcher MessageMatcher { get; set; } = null!;
/// <summary> /// <summary>
/// Cancellation token registration /// Cancellation token registration
@@ -73,25 +74,29 @@ namespace CryptoExchange.Net.Sockets
/// Exception event /// Exception event
/// </summary> /// </summary>
public event Action<Exception>? Exception; public event Action<Exception>? Exception;
/// <summary> /// <summary>
/// Get the deserialization type for this message /// Listener unsubscribed event
/// </summary> /// </summary>
/// <param name="message"></param> public event Action? Unsubscribed;
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary> /// <summary>
/// Subscription topic /// Subscription topic
/// </summary> /// </summary>
public string? Topic { get; set; } public string? Topic { get; set; }
/// <summary>
/// The subscribe query for this subscription
/// </summary>
public Query? SubscriptionQuery { get; private set; }
/// <summary>
/// The unsubscribe query for this subscription
/// </summary>
public Query? UnsubscriptionQuery { get; private set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
/// <param name="userSubscription"></param>
public Subscription(ILogger logger, bool authenticated, bool userSubscription = true) public Subscription(ILogger logger, bool authenticated, bool userSubscription = true)
{ {
_logger = logger; _logger = logger;
@@ -100,11 +105,21 @@ namespace CryptoExchange.Net.Sockets
Id = ExchangeHelpers.NextId(); Id = ExchangeHelpers.NextId();
} }
/// <summary>
/// Create a new subscription query
/// </summary>
public Query? CreateSubscriptionQuery(SocketConnection connection)
{
var query = GetSubQuery(connection);
SubscriptionQuery = query;
return query;
}
/// <summary> /// <summary>
/// Get the subscribe query to send when subscribing /// Get the subscribe query to send when subscribing
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public abstract Query? GetSubQuery(SocketConnection connection); protected abstract Query? GetSubQuery(SocketConnection connection);
/// <summary> /// <summary>
/// Handle a subscription query response /// Handle a subscription query response
@@ -118,11 +133,21 @@ namespace CryptoExchange.Net.Sockets
/// <param name="message"></param> /// <param name="message"></param>
public virtual void HandleUnsubQueryResponse(object message) { } public virtual void HandleUnsubQueryResponse(object message) { }
/// <summary>
/// Create a new unsubscription query
/// </summary>
public Query? CreateUnsubscriptionQuery(SocketConnection connection)
{
var query = GetUnsubQuery(connection);
UnsubscriptionQuery = query;
return query;
}
/// <summary> /// <summary>
/// Get the unsubscribe query to send when unsubscribing /// Get the unsubscribe query to send when unsubscribing
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public abstract Query? GetUnsubQuery(); protected abstract Query? GetUnsubQuery(SocketConnection connection);
/// <inheritdoc /> /// <inheritdoc />
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
@@ -130,14 +155,11 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Handle an update message /// Handle an update message
/// </summary> /// </summary>
/// <param name="connection"></param> public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matcher)
/// <param name="message"></param>
/// <returns></returns>
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
{ {
ConnectionInvocations++; ConnectionInvocations++;
TotalInvocations++; TotalInvocations++;
return Task.FromResult(DoHandleMessage(connection, message)); return Task.FromResult(matcher.Handle(connection, message));
} }
/// <summary> /// <summary>
@@ -154,14 +176,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public virtual void DoHandleReset() { } public virtual void DoHandleReset() { }
/// <summary>
/// Handle the update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message);
/// <summary> /// <summary>
/// Invoke the exception event /// Invoke the exception event
/// </summary> /// </summary>
@@ -171,18 +185,26 @@ namespace CryptoExchange.Net.Sockets
Exception?.Invoke(e); Exception?.Invoke(e);
} }
/// <summary>
/// Invoke the unsubscribed event
/// </summary>
public void InvokeUnsubscribedHandler()
{
Unsubscribed?.Invoke();
}
/// <summary> /// <summary>
/// State of this subscription /// State of this subscription
/// </summary> /// </summary>
/// <param name="Id">The id of the subscription</param> /// <param name="Id">The id of the subscription</param>
/// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param> /// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param>
/// <param name="Invocations">Number of times this subscription got a message</param> /// <param name="Invocations">Number of times this subscription got a message</param>
/// <param name="Identifiers">Identifiers the subscription is listening to</param> /// <param name="ListenMatcher">Matcher for this subscription</param>
public record SubscriptionState( public record SubscriptionState(
int Id, int Id,
bool Confirmed, bool Confirmed,
int Invocations, int Invocations,
HashSet<string> Identifiers MessageMatcher ListenMatcher
); );
/// <summary> /// <summary>
@@ -191,7 +213,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public SubscriptionState GetState() public SubscriptionState GetState()
{ {
return new SubscriptionState(Id, Confirmed, TotalInvocations, ListenerIdentifiers); return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher);
} }
} }
@@ -22,37 +22,9 @@ namespace CryptoExchange.Net.Sockets
} }
/// <inheritdoc /> /// <inheritdoc />
public override Query? GetSubQuery(SocketConnection connection) => null; protected override Query? GetSubQuery(SocketConnection connection) => null;
/// <inheritdoc /> /// <inheritdoc />
public override Query? GetUnsubQuery() => null; protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
/// <inheritdoc />
public abstract class SystemSubscription<T> : SystemSubscription
{
/// <inheritdoc />
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
/// <inheritdoc />
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
=> HandleMessage(connection, message.As((T)message.Data));
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
protected SystemSubscription(ILogger logger, bool authenticated) : base(logger, authenticated)
{
}
/// <summary>
/// Handle an update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract CallResult HandleMessage(SocketConnection connection, DataEvent<T> message);
} }
} }
@@ -386,7 +386,7 @@ namespace CryptoExchange.Net.Testing.Comparers
var stringValue = jsonValue.GetString(); var stringValue = jsonValue.GetString();
if (objectValue is decimal dec) if (objectValue is decimal dec)
{ {
if (decimal.Parse(stringValue!, CultureInfo.InvariantCulture) != dec) if (ExchangeHelpers.ParseDecimal(stringValue!) != dec)
throw new Exception($"{method}: {property} not equal: {stringValue} vs {dec}"); throw new Exception($"{method}: {property} not equal: {stringValue} vs {dec}");
} }
else if (objectValue is DateTime time) else if (objectValue is DateTime time)
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Testing.Implementations
public Uri Uri { get; set; } public Uri Uri { get; set; }
public Version HttpVersion { get; set; }
public int RequestId { get; set; } public int RequestId { get; set; }
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System; using System;
using System.Net.Http; using System.Net.Http;
@@ -14,11 +15,11 @@ namespace CryptoExchange.Net.Testing.Implementations
_request = request; _request = request;
} }
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null) public void Configure(RestExchangeOptions options, HttpClient? client)
{ {
} }
public IRequest Create(HttpMethod method, Uri uri, int requestId) public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
{ {
_request.Method = method; _request.Method = method;
_request.Uri = uri; _request.Uri = uri;
@@ -26,6 +27,6 @@ namespace CryptoExchange.Net.Testing.Implementations
return _request; return _request;
} }
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {} public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval) {}
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.Testing.Implementations
private readonly Stream _response; private readonly Stream _response;
public HttpStatusCode StatusCode { get; } public HttpStatusCode StatusCode { get; }
public Version HttpVersion { get; }
public bool IsSuccessStatusCode { get; } public bool IsSuccessStatusCode { get; }
@@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Testing.Implementations
public TestResponse(HttpStatusCode code, Stream response) public TestResponse(HttpStatusCode code, Stream response)
{ {
StatusCode = code; StatusCode = code;
HttpVersion = new Version(2, 0);
IsSuccessStatusCode = code == HttpStatusCode.OK; IsSuccessStatusCode = code == HttpStatusCode.OK;
_response = response; _response = response;
} }
+17 -13
View File
@@ -128,23 +128,27 @@ namespace CryptoExchange.Net.Testing
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null; var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null;
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null; var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null;
var headers = new Dictionary<string, string>(); var requestDefinition = new RestRequestConfiguration(
new RequestDefinition(path, method)
{
Authenticated = true
},
host,
uriParams ?? new Dictionary<string, object>(),
bodyParams ?? new Dictionary<string, object>(),
new Dictionary<string, string>(),
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
);
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc)); authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
authProvider.AuthenticateRequest( authProvider.ProcessRequest(
client, client,
new Uri(host.AppendPath(path)), requestDefinition
method,
ref uriParams,
ref bodyParams,
ref headers,
true,
client.ArraySerialization,
client.ParameterPositions[method],
client.RequestBodyFormat
); );
var signature = getSignature(uriParams, bodyParams, headers); var signature = getSignature(requestDefinition.QueryParameters, requestDefinition.BodyParameters, requestDefinition.Headers);
if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}"); throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
@@ -176,18 +176,32 @@ namespace CryptoExchange.Net.Trackers.Klines
Status = SyncStatus.Syncing; Status = SyncStatus.Syncing;
_logger.KlineTrackerStarting(SymbolName); _logger.KlineTrackerStarting(SymbolName);
var startResult = await DoStartAsync().ConfigureAwait(false); var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
if (!startResult) update =>
{
AddOrUpdate(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{ {
_logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.Message, startResult.Error.Exception); _logger.KlineTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception);
Status = SyncStatus.Disconnected; Status = SyncStatus.Disconnected;
return new CallResult(startResult.Error!); return subResult;
} }
_updateSubscription = startResult.Data; _updateSubscription = subResult.Data;
_updateSubscription.ConnectionLost += HandleConnectionLost; _updateSubscription.ConnectionLost += HandleConnectionLost;
_updateSubscription.ConnectionClosed += HandleConnectionClosed; _updateSubscription.ConnectionClosed += HandleConnectionClosed;
_updateSubscription.ConnectionRestored += HandleConnectionRestored; _updateSubscription.ConnectionRestored += HandleConnectionRestored;
var startResult = await DoStartAsync().ConfigureAwait(false);
if (!startResult)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return new CallResult(startResult.Error!);
}
Status = SyncStatus.Synced; Status = SyncStatus.Synced;
_logger.KlineTrackerStarted(SymbolName); _logger.KlineTrackerStarted(SymbolName);
return CallResult.SuccessResult; return CallResult.SuccessResult;
@@ -208,22 +222,10 @@ namespace CryptoExchange.Net.Trackers.Klines
/// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot /// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync() protected virtual async Task<CallResult> DoStartAsync()
{ {
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
update =>
{
AddOrUpdate(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{
Status = SyncStatus.Disconnected;
return subResult;
}
if (!_startWithSnapshot) if (!_startWithSnapshot)
return subResult; return CallResult.SuccessResult;
var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value); var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value);
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime) if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
@@ -236,11 +238,7 @@ namespace CryptoExchange.Net.Trackers.Klines
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false)) await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
{ {
if (!result) if (!result)
{ return result;
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return subResult.AsError<UpdateSubscription>(result.Error!);
}
if (Limit != null && data.Count > Limit) if (Limit != null && data.Count > Limit)
break; break;
@@ -249,7 +247,7 @@ namespace CryptoExchange.Net.Trackers.Klines
} }
SetInitialData(data); SetInitialData(data);
return subResult; return CallResult.SuccessResult;
} }
/// <summary> /// <summary>
@@ -199,10 +199,15 @@ namespace CryptoExchange.Net.Trackers.Trades
_startWithSnapshot = startWithSnapshot; _startWithSnapshot = startWithSnapshot;
Status = SyncStatus.Syncing; Status = SyncStatus.Syncing;
_logger.TradeTrackerStarting(SymbolName); _logger.TradeTrackerStarting(SymbolName);
var subResult = await DoStartAsync().ConfigureAwait(false); var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
update =>
{
AddData(update.Data);
}).ConfigureAwait(false);
if (!subResult) if (!subResult)
{ {
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception); _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception);
Status = SyncStatus.Disconnected; Status = SyncStatus.Disconnected;
return subResult; return subResult;
} }
@@ -211,6 +216,15 @@ namespace CryptoExchange.Net.Trackers.Trades
_updateSubscription.ConnectionLost += HandleConnectionLost; _updateSubscription.ConnectionLost += HandleConnectionLost;
_updateSubscription.ConnectionClosed += HandleConnectionClosed; _updateSubscription.ConnectionClosed += HandleConnectionClosed;
_updateSubscription.ConnectionRestored += HandleConnectionRestored; _updateSubscription.ConnectionRestored += HandleConnectionRestored;
var result = await DoStartAsync().ConfigureAwait(false);
if (!result)
{
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return result;
}
SetSyncStatus(); SetSyncStatus();
_logger.TradeTrackerStarted(SymbolName); _logger.TradeTrackerStarted(SymbolName);
return CallResult.SuccessResult; return CallResult.SuccessResult;
@@ -231,22 +245,10 @@ namespace CryptoExchange.Net.Trackers.Trades
/// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot /// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync() protected virtual async Task<CallResult> DoStartAsync()
{ {
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
update =>
{
AddData(update.Data);
}).ConfigureAwait(false);
if (!subResult)
{
Status = SyncStatus.Disconnected;
return subResult;
}
if (!_startWithSnapshot) if (!_startWithSnapshot)
return subResult; return CallResult.SuccessResult;
if (_historyRestClient != null) if (_historyRestClient != null)
{ {
@@ -256,12 +258,8 @@ namespace CryptoExchange.Net.Trackers.Trades
await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false)) await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
{ {
if (!result) if (!result)
{ return result;
_ = subResult.Data.CloseAsync();
Status = SyncStatus.Disconnected;
return subResult.AsError<UpdateSubscription>(result.Error!);
}
if (Limit != null && data.Count > Limit) if (Limit != null && data.Count > Limit)
break; break;
@@ -279,15 +277,13 @@ namespace CryptoExchange.Net.Trackers.Trades
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false); var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false);
if (!snapshot) if (!snapshot)
{ {
_ = subResult.Data.CloseAsync(); return snapshot;
Status = SyncStatus.Disconnected;
return subResult.AsError<UpdateSubscription>(snapshot.Error!);
} }
SetInitialData(snapshot.Data); SetInitialData(snapshot.Data);
} }
return subResult; return CallResult.SuccessResult;
} }
/// <summary> /// <summary>
+1 -1
View File
@@ -1,4 +1,4 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found> </Found>
+23 -21
View File
@@ -5,28 +5,30 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="11.1.0" /> <PackageReference Include="Binance.Net" Version="11.7.1" />
<PackageReference Include="Bitfinex.Net" Version="9.1.0" /> <PackageReference Include="Bitfinex.Net" Version="9.7.0" />
<PackageReference Include="BitMart.Net" Version="2.1.0" /> <PackageReference Include="BitMart.Net" Version="2.8.0" />
<PackageReference Include="Bybit.Net" Version="5.1.0" /> <PackageReference Include="BloFin.Net" Version="1.0.0" />
<PackageReference Include="CoinEx.Net" Version="9.1.0" /> <PackageReference Include="Bybit.Net" Version="5.8.0" />
<PackageReference Include="CryptoCom.Net" Version="2.1.0" /> <PackageReference Include="CoinEx.Net" Version="9.7.0" />
<PackageReference Include="DeepCoin.Net" Version="2.1.0" /> <PackageReference Include="CoinW.Net" Version="1.4.0" />
<PackageReference Include="GateIo.Net" Version="2.1.0" /> <PackageReference Include="CryptoCom.Net" Version="2.8.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.1.1" /> <PackageReference Include="DeepCoin.Net" Version="2.7.0" />
<PackageReference Include="JK.BingX.Net" Version="2.1.0" /> <PackageReference Include="GateIo.Net" Version="2.8.1" />
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" /> <PackageReference Include="HyperLiquid.Net" Version="2.12.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" /> <PackageReference Include="JK.BingX.Net" Version="2.7.0" />
<PackageReference Include="JK.OKX.Net" Version="3.1.0" /> <PackageReference Include="JK.Bitget.Net" Version="2.7.1" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.1.0" /> <PackageReference Include="JK.Mexc.Net" Version="3.8.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" /> <PackageReference Include="JK.OKX.Net" Version="3.7.1" />
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" /> <PackageReference Include="JKorf.BitMEX.Net" Version="2.7.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="2.7.0" />
<PackageReference Include="Kucoin.Net" Version="7.1.0" /> <PackageReference Include="JKorf.HTX.Net" Version="7.7.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.7.0" />
<PackageReference Include="Kucoin.Net" Version="7.7.1" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Toobit.Net" Version="1.0.1" /> <PackageReference Include="Toobit.Net" Version="1.6.0" />
<PackageReference Include="WhiteBit.Net" Version="2.1.0" /> <PackageReference Include="WhiteBit.Net" Version="2.8.0" />
<PackageReference Include="XT.Net" Version="2.1.0" /> <PackageReference Include="XT.Net" Version="2.7.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+10
View File
@@ -5,9 +5,11 @@
@inject IBitgetRestClient bitgetClient @inject IBitgetRestClient bitgetClient
@inject IBitMartRestClient bitmartClient @inject IBitMartRestClient bitmartClient
@inject IBitMEXRestClient bitmexClient @inject IBitMEXRestClient bitmexClient
@inject IBloFinRestClient bloFinClient
@inject IBybitRestClient bybitClient @inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient @inject ICoinbaseRestClient coinbaseClient
@inject ICoinExRestClient coinexClient @inject ICoinExRestClient coinexClient
@inject ICoinWRestClient coinWClient
@inject ICryptoComRestClient cryptocomClient @inject ICryptoComRestClient cryptocomClient
@inject IDeepCoinRestClient deepCoinClient @inject IDeepCoinRestClient deepCoinClient
@inject IGateIoRestClient gateioClient @inject IGateIoRestClient gateioClient
@@ -38,9 +40,11 @@
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT"); var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT"); var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT"); var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT"); var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT"); var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]); var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync();
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT"); var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot); var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT"); var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
@@ -74,6 +78,9 @@
if (bitmexTask.Result.Success) if (bitmexTask.Result.Success)
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice); _prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
if (bloFinTask.Result.Success)
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
if (bybitTask.Result.Success) if (bybitTask.Result.Success)
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice); _prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
@@ -83,6 +90,9 @@
if (coinexTask.Result.Success) if (coinexTask.Result.Success)
_prices.Add("CoinEx", coinexTask.Result.Data.Single().LastPrice); _prices.Add("CoinEx", coinexTask.Result.Data.Single().LastPrice);
if (coinWTask.Result.Success)
_prices.Add("CoinW", coinWTask.Result.Data.Single(x => x.Symbol == "BTC_USDT").LastPrice);
if (cryptocomTask.Result.Success) if (cryptocomTask.Result.Success)
_prices.Add("CryptoCom", cryptocomTask.Result.Data.First().LastPrice ?? 0); _prices.Add("CryptoCom", cryptocomTask.Result.Data.First().LastPrice ?? 0);
+7 -1
View File
@@ -5,9 +5,11 @@
@inject IBitgetSocketClient bitgetSocketClient @inject IBitgetSocketClient bitgetSocketClient
@inject IBitMartSocketClient bitmartSocketClient @inject IBitMartSocketClient bitmartSocketClient
@inject IBitMEXSocketClient bitmexSocketClient @inject IBitMEXSocketClient bitmexSocketClient
@inject IBloFinSocketClient bloFinSocketClient
@inject IBybitSocketClient bybitSocketClient @inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient @inject ICoinbaseSocketClient coinbaseSocketClient
@inject ICoinExSocketClient coinExSocketClient @inject ICoinExSocketClient coinExSocketClient
@inject ICoinWSocketClient coinWSocketClient
@inject ICryptoComSocketClient cryptocomSocketClient @inject ICryptoComSocketClient cryptocomSocketClient
@inject IDeepCoinSocketClient deepCoinSocketClient @inject IDeepCoinSocketClient deepCoinSocketClient
@inject IGateIoSocketClient gateioSocketClient @inject IGateIoSocketClient gateioSocketClient
@@ -47,8 +49,11 @@
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)), bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)), bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)), bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
// BloFin doesn't support the ETH/BTC pair
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)), bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)), coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)), coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)),
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)), cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)), deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
@@ -59,7 +64,8 @@
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)), //hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)), krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)), kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)), // Mexc doesn't offer a ticker stream currently
//mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)), okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
// Toobit doesn't support the ETH/BTC pair // Toobit doesn't support the ETH/BTC pair
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)), //toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
+11 -8
View File
@@ -7,8 +7,10 @@
@using Bitget.Net.Interfaces; @using Bitget.Net.Interfaces;
@using BitMart.Net.Interfaces; @using BitMart.Net.Interfaces;
@using BitMEX.Net.Interfaces; @using BitMEX.Net.Interfaces;
@using BloFin.Net.Interfaces;
@using Bybit.Net.Interfaces @using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces @using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces @using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Authentication @using CryptoExchange.Net.Authentication
@using CryptoExchange.Net.Interfaces @using CryptoExchange.Net.Interfaces
@@ -31,9 +33,11 @@
@inject IBitgetOrderBookFactory bitgetFactory @inject IBitgetOrderBookFactory bitgetFactory
@inject IBitMartOrderBookFactory bitmartFactory @inject IBitMartOrderBookFactory bitmartFactory
@inject IBitMEXOrderBookFactory bitmexFactory @inject IBitMEXOrderBookFactory bitmexFactory
@inject IBloFinOrderBookFactory bloFinFactory
@inject IBybitOrderBookFactory bybitFactory @inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory @inject ICoinbaseOrderBookFactory coinbaseFactory
@inject ICoinExOrderBookFactory coinExFactory @inject ICoinExOrderBookFactory coinExFactory
@inject ICoinWOrderBookFactory coinWFactory
@inject ICryptoComOrderBookFactory cryptocomFactory @inject ICryptoComOrderBookFactory cryptocomFactory
@inject IDeepCoinOrderBookFactory deepCoinFactory @inject IDeepCoinOrderBookFactory deepCoinFactory
@inject IGateIoOrderBookFactory gateioFactory @inject IGateIoOrderBookFactory gateioFactory
@@ -53,7 +57,7 @@
@foreach(var book in _books.OrderBy(p => p.Key)) @foreach(var book in _books.OrderBy(p => p.Key))
{ {
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;"> <div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
<h4>@book.Key</h4> <h4>@book.Key - @book.Value.Symbol</h4>
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3) @if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
{ {
for (var i = 0; i < 3; i++) for (var i = 0; i < 3; i++)
@@ -85,22 +89,21 @@
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") }, { "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) }, { "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
{ "BitMEX", bitmexFactory.Create("ETH_XBT") }, { "BitMEX", bitmexFactory.Create("ETH_XBT") },
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) }, { "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) }, { "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") }, { "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") }, { "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") }, { "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
// DeepCoin does not support the ETH/BTC pair { "DeepCoin", deepCoinFactory.Create("ETH-USDT") },
//{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
{ "HTX", htxFactory.CreateSpot("ethbtc") }, { "HTX", htxFactory.CreateSpot("ethbtc") },
// HyperLiquid does not support the ETH/BTC pair { "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
//{ "HyperLiquid", hyperLiquidFactory.Create("ETH/BTC") },
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") }, { "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") }, { "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") }, { "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") }, { "OKX", okxFactory.Create("ETH-BTC") },
// Toobit does not support the ETH/BTC pair { "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") }, { "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
{ "XT", xtFactory.CreateSpot("eth_btc") }, { "XT", xtFactory.CreateSpot("eth_btc") },
}; };
@@ -115,7 +118,7 @@
public void Dispose() public void Dispose()
{ {
_timer.Stop(); _timer?.Stop();
_timer.Dispose(); _timer.Dispose();
foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected)) foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
// It's not necessary to wait for this // It's not necessary to wait for this
+32 -25
View File
@@ -7,8 +7,10 @@
@using Bitget.Net.Interfaces; @using Bitget.Net.Interfaces;
@using BitMEX.Net.Interfaces; @using BitMEX.Net.Interfaces;
@using BitMart.Net.Interfaces; @using BitMart.Net.Interfaces;
@using BloFin.Net.Interfaces
@using Bybit.Net.Interfaces @using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces @using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces @using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Interfaces @using CryptoExchange.Net.Interfaces
@using CryptoCom.Net.Interfaces @using CryptoCom.Net.Interfaces
@@ -32,9 +34,11 @@
@inject IBitgetTrackerFactory bitgetFactory @inject IBitgetTrackerFactory bitgetFactory
@inject IBitMartTrackerFactory bitmartFactory @inject IBitMartTrackerFactory bitmartFactory
@inject IBitMEXTrackerFactory bitmexFactory @inject IBitMEXTrackerFactory bitmexFactory
@inject IBloFinTrackerFactory bloFinFactory
@inject IBybitTrackerFactory bybitFactory @inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory @inject ICoinbaseTrackerFactory coinbaseFactory
@inject ICoinExTrackerFactory coinExFactory @inject ICoinExTrackerFactory coinExFactory
@inject ICoinWTrackerFactory coinWFactory
@inject ICryptoComTrackerFactory cryptocomFactory @inject ICryptoComTrackerFactory cryptocomFactory
@inject IDeepCoinTrackerFactory deepCoinFactory @inject IDeepCoinTrackerFactory deepCoinFactory
@inject IGateIoTrackerFactory gateioFactory @inject IGateIoTrackerFactory gateioFactory
@@ -69,32 +73,35 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var usdtSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var usdcSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDC");
var usdtSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var usdtPerpLinSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
_trackers = new List<ITradeTracker> _trackers = new List<ITradeTracker>
{ {
{ binanceFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bingXFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitfinexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitgetFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bitgetFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitmartFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bitmartFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitmexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bitmexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bloFinFactory.CreateTradeTracker(usdtPerpLinSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { bybitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { coinbaseFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { coinExFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { coinWFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { cryptocomFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ htxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { deepCoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
// HyperLiquid doesn't support spot pair, but does have a futures BTC/USDC pair { gateioFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ hyperLiquidFactory.CreateTradeTracker(new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDC"), period: TimeSpan.FromMinutes(5)) }, { htxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ krakenFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { hyperLiquidFactory.CreateTradeTracker(usdcSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { krakenFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { kucoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { toobitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) }, { whitebitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
}; };
await Task.WhenAll(_trackers.Select(b => b.StartAsync())); await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
@@ -121,8 +128,8 @@
public void Dispose() public void Dispose()
{ {
_timer.Stop(); _timer?.Stop();
_timer.Dispose(); _timer?.Dispose();
foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected)) foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected))
// It's not necessary to wait for this // It's not necessary to wait for this
_ = tracker.StopAsync(); _ = tracker.StopAsync();
+2
View File
@@ -38,9 +38,11 @@ namespace BlazorClient
services.AddBitget(); services.AddBitget();
services.AddBitMart(); services.AddBitMart();
services.AddBitMEX(); services.AddBitMEX();
services.AddBloFin();
services.AddBybit(); services.AddBybit();
services.AddCoinbase(); services.AddCoinbase();
services.AddCoinEx(); services.AddCoinEx();
services.AddCoinW();
services.AddCryptoCom(); services.AddCryptoCom();
services.AddDeepCoin(); services.AddDeepCoin();
services.AddGateIo(); services.AddGateIo();
+2
View File
@@ -14,9 +14,11 @@
@using Bitget.Net.Interfaces.Clients; @using Bitget.Net.Interfaces.Clients;
@using BitMart.Net.Interfaces.Clients; @using BitMart.Net.Interfaces.Clients;
@using BitMEX.Net.Interfaces.Clients; @using BitMEX.Net.Interfaces.Clients;
@using BloFin.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients; @using Bybit.Net.Interfaces.Clients;
@using Coinbase.Net.Interfaces.Clients; @using Coinbase.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients; @using CoinEx.Net.Interfaces.Clients;
@using CoinW.Net.Interfaces.Clients;
@using CryptoCom.Net.Interfaces.Clients; @using CryptoCom.Net.Interfaces.Clients;
@using DeepCoin.Net.Interfaces.Clients; @using DeepCoin.Net.Interfaces.Clients;
@using GateIo.Net.Interfaces.Clients; @using GateIo.Net.Interfaces.Clients;
+52
View File
@@ -18,9 +18,11 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%| |![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%| |![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%| |![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-|
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-| |![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-| |![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|![CoinEx](https://raw.githubusercontent.com/JKorf/CoinEx.Net/refs/heads/master/CoinEx.Net/Icon/icon.png)|CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%| |![CoinEx](https://raw.githubusercontent.com/JKorf/CoinEx.Net/refs/heads/master/CoinEx.Net/Icon/icon.png)|CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
|![CoinW](https://raw.githubusercontent.com/JKorf/CoinW.Net/refs/heads/main/CoinW.Net/Icon/icon.png)|CoinW|CEX|[JKorf/CoinW.Net](https://github.com/JKorf/CoinW.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinW.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinW.Net)|[Link](https://www.coinw.com/register?rc=rbtnp)|-|
|![CoinGecko](https://raw.githubusercontent.com/JKorf/CoinGecko.Net/refs/heads/main/CoinGecko.Net/Icon/icon.png)|CoinGecko|-|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|-|-| |![CoinGecko](https://raw.githubusercontent.com/JKorf/CoinGecko.Net/refs/heads/main/CoinGecko.Net/Icon/icon.png)|CoinGecko|-|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|-|-|
|![Crypto.com](https://raw.githubusercontent.com/JKorf/CryptoCom.Net/refs/heads/main/CryptoCom.Net/Icon/icon.png)|Crypto.com|CEX|[JKorf/CryptoCom.Net](https://github.com/JKorf/CryptoCom.Net)|[![Nuget version](https://img.shields.io/nuget/v/CryptoCom.net.svg?style=flat-square)](https://www.nuget.org/packages/CryptoCom.Net)|[Link](https://crypto.com/exch/26ge92xbkn)|-| |![Crypto.com](https://raw.githubusercontent.com/JKorf/CryptoCom.Net/refs/heads/main/CryptoCom.Net/Icon/icon.png)|Crypto.com|CEX|[JKorf/CryptoCom.Net](https://github.com/JKorf/CryptoCom.Net)|[![Nuget version](https://img.shields.io/nuget/v/CryptoCom.net.svg?style=flat-square)](https://www.nuget.org/packages/CryptoCom.Net)|[Link](https://crypto.com/exch/26ge92xbkn)|-|
|![DeepCoin](https://raw.githubusercontent.com/JKorf/DeepCoin.Net/refs/heads/main/DeepCoin.Net/Icon/icon.png)|DeepCoin|CEX|[JKorf/DeepCoin.Net](https://github.com/JKorf/DeepCoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/DeepCoin.net.svg?style=flat-square)](https://www.nuget.org/packages/DeepCoin.Net)|[Link](https://s.deepcoin.com/jddhfca)|-| |![DeepCoin](https://raw.githubusercontent.com/JKorf/DeepCoin.Net/refs/heads/main/DeepCoin.Net/Icon/icon.png)|DeepCoin|CEX|[JKorf/DeepCoin.Net](https://github.com/JKorf/DeepCoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/DeepCoin.net.svg?style=flat-square)](https://www.nuget.org/packages/DeepCoin.Net)|[Link](https://s.deepcoin.com/jddhfca)|-|
@@ -37,6 +39,10 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's. Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
### Full demo application
A full demo application is available using the [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) library:
https://github.com/JKorf/CryptoManager.Net
## Discord ## Discord
[![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](https://discord.gg/MSpeEtSY8t) [![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](https://discord.gg/MSpeEtSY8t)
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries. A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
@@ -58,6 +64,52 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 9.8.0 - 30 Sep 2025
* Added ContractAddress to SharedAsset model
* Added ITrackerFactory interface
* Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed
* Fixed ExchangeHelpers.AdjustValueStep high precision calculation
* Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect
* Fixed EnumConverter to allow mapping empty string values
* Version 9.7.0 - 01 Sep 2025
* Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
* Added REST client option for selecting HTTP protocol version
* Added REST client option for HTTP client keep alive interval
* Added HttpVersion to WebCallResult responses
* Updated request logic to default to using HTTP version 2.0 for dotnet core
* Version 9.6.0 - 25 Aug 2025
* Added support for parsing REST response even though status indicates error
* Added better support for subscriptions without subscribe confirmation
* Added check in websocket for receiving 401 unauthorized http response status when 101 was expected
* Removed obsolete attribute on Error.Code property, updated the description
* Version 9.5.0 - 19 Aug 2025
* Added better error handling support
* Added ErrorDescription, ErrorType and IsTransient to Error object
* Added ErrorCode in favor of Code
* Updated some error messages
* Refactored RestApiClient request authentication and AuthenticationProvider to prevent duplicate query string / body serialization
* Fixed IOrderBookSocketClient Shared interface not getting registered in DI
* Fixed response type in websocket queries not interested in the response
* Fixed timing issue in query response processing
* Version 9.4.0 - 04 Aug 2025
* Updated Shared symbol requests/subscriptions to allow multiple symbols in one call if supported
* Version 9.3.1 - 29 Jul 2025
* Added BaseAndQuoteAssetAndContracts value to SharedQuantityType enum
* Added Id property to SharedPosition model
* Version 9.3.0 - 23 Jul 2025
* Updated websocket message to listener matching logic to be more flexible
* Updated decimal parser to support "NaN" and "-Infinity" strings, added check for negative overflow value, improved performance in most cases
* Version 9.2.1 - 16 Jul 2025
* Added setting for whether or not to process unparsable websocket messages
* Fixed issue causing duplicate subscriptions and data in the TradeTracker and KlineTracker when websocket connection was reconnected
* Version 9.2.0 - 14 Jul 2025 * Version 9.2.0 - 14 Jul 2025
* Added support for sending byte data on websocket * Added support for sending byte data on websocket
* Added support for handling both string and byte data with different IMessageAccessor types * Added support for handling both string and byte data with different IMessageAccessor types