mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-11 16:32:57 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d433ff7475 | |||
| 71957037d0 | |||
| bcdcdbbd4e | |||
| 1ece13f5bc | |||
| da70ba6ec7 | |||
| a832f0e4d4 | |||
| 6ab4d005a0 | |||
| 18dc935038 | |||
| bb3a534f75 | |||
| 649ba370c6 | |||
| 51732c5ce6 | |||
| 0ba7b46680 | |||
| 94dfbb7b9e | |||
| b8b7512b35 | |||
| aba6b773ce | |||
| d88fb0d356 | |||
| b8c6d55156 | |||
| d9a5481db2 | |||
| 6a8bb42c0e | |||
| 2445f001ab | |||
| c84fa9ac32 | |||
| d44a11c44e | |||
| b215cccda4 | |||
| 3eda488361 | |||
| 993a44de35 | |||
| 99465f99a1 | |||
| d42de1fe90 | |||
| d0284c62c0 | |||
| d92f3b7904 | |||
| 3e1b5ada69 | |||
| 6156fb8154 | |||
| f2753aed1e | |||
| e33d826381 | |||
| 364aa4d324 | |||
| 455b332757 | |||
| 876b895645 | |||
| daf7ed9fe6 | |||
| 3e365f83c9 | |||
| 40977ebdbe | |||
| 4a9058fc1c | |||
| dab9a21608 | |||
| a89c222399 | |||
| 1e356d2a45 | |||
| eed794c2cf | |||
| 2f82e2015b | |||
| ad599badb2 | |||
| 1e45c73f1d | |||
| 49c1fda2c1 | |||
| 32a31e464b | |||
| cddb4167e4 | |||
| 65457d8df2 | |||
| 122a6cad43 | |||
| 4c0e841425 | |||
| 92f5839aec | |||
| 30475dae67 | |||
| 3d942bd503 | |||
| f739520e52 | |||
| 0152603ddb | |||
| aa06e0eead | |||
| 2fde9a285e | |||
| b9f6eb6abb |
@@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Converters.Protobuf
|
||||
}
|
||||
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)
|
||||
{
|
||||
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
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
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>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>9.2.0</PackageVersion>
|
||||
<AssemblyVersion>9.2.0</AssemblyVersion>
|
||||
<FileVersion>9.2.0</FileVersion>
|
||||
<PackageVersion>9.9.0</PackageVersion>
|
||||
<AssemblyVersion>9.9.0</AssemblyVersion>
|
||||
<FileVersion>9.9.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -41,7 +41,7 @@
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="9.2.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.52" />
|
||||
<PackageReference Include="CryptoExchange.Net" Version="9.9.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,9 +1,31 @@
|
||||
#  CryptoExchange.Net.Proto
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.NetProtobuf/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.NetProtobuf) 
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
||||
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* 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
|
||||
* Initial release
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -16,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
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.Success);
|
||||
}
|
||||
@@ -36,9 +38,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
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.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
@@ -71,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
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);
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
@@ -84,11 +86,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
public void TestCallResultErrorAsError()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
@@ -97,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[Test]
|
||||
public void TestWebCallResultErrorAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
@@ -112,6 +114,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new KeyValuePair<string, string[]>[0],
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
@@ -124,10 +127,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
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.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
@@ -142,6 +145,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new KeyValuePair<string, string[]>[0],
|
||||
TimeSpan.FromSeconds(1),
|
||||
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.Down, 0.5516592, 0.5516)]
|
||||
[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)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test1", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/api/test1", false)]
|
||||
[TestCase("sapi/test1", false)]
|
||||
[TestCase("sapi/test1", true)]
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
|
||||
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
||||
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
@@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
||||
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,13 +224,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("null", null)]
|
||||
[TestCase("nan", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[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)
|
||||
{
|
||||
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)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
@@ -31,21 +32,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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>
|
||||
{
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
||||
|
||||
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
ListenerIdentifiers = new HashSet<string> { identifier };
|
||||
MessageMatcher = MessageMatcher.Create<object>(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
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)
|
||||
{
|
||||
_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.As(data));
|
||||
_handler.Invoke(message);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
||||
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
|
||||
protected 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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-9
@@ -15,24 +15,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
private readonly string _channel;
|
||||
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
||||
|
||||
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;
|
||||
_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.As(data));
|
||||
_handler.Invoke(message);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
||||
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
||||
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
||||
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
||||
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
@@ -55,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var accessor = CreateAccessor();
|
||||
var valid = accessor.Read(stream, true).Result;
|
||||
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>();
|
||||
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 GetSecret() => _credentials.Secret;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json.Serialization;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -59,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
||||
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
@@ -68,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -117,13 +118,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
||||
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
}
|
||||
@@ -197,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -51,30 +51,11 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
||||
/// Authenticate a request
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for the request</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="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
|
||||
);
|
||||
/// <param name="requestConfig">The request configuration</param>
|
||||
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -54,6 +56,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of a response code to known error types
|
||||
/// </summary>
|
||||
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -87,6 +94,16 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <inheritdoc />
|
||||
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 />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using CryptoExchange.Net.Caching;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
@@ -54,7 +55,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether parameters need to be ordered
|
||||
@@ -105,7 +106,7 @@ namespace CryptoExchange.Net.Clients
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
||||
RequestFactory.Configure(options, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -238,7 +239,7 @@ namespace CryptoExchange.Net.Clients
|
||||
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)}]")));
|
||||
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)
|
||||
{
|
||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||
@@ -363,74 +364,58 @@ namespace CryptoExchange.Net.Clients
|
||||
ParameterCollection? bodyParameters,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
|
||||
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
|
||||
var requestConfiguration = new RestRequestConfiguration(
|
||||
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));
|
||||
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
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
|
||||
}
|
||||
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
|
||||
if (uriParams != null)
|
||||
uri = uri.SetParameters(uriParams, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
||||
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
foreach (var header in requestConfiguration.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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
request.SetContent(bodyContent, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParams != null && bodyParams.Count != 0)
|
||||
WriteParamBody(request, bodyParams, contentType);
|
||||
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;
|
||||
@@ -439,11 +424,13 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <summary>
|
||||
/// Executes the request and returns the result deserialized into the type parameter class
|
||||
/// </summary>
|
||||
/// <param name="requestDefinition">The request definition</param>
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="gate">The ratelimit gate used</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||
RequestDefinition requestDefinition,
|
||||
IRequest request,
|
||||
IRateLimitGate? gate,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -456,14 +443,11 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
var statusCode = response.StatusCode;
|
||||
var headers = response.ResponseHeaders;
|
||||
var responseLength = response.ContentLength;
|
||||
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||
|
||||
accessor = CreateAccessor();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||
{
|
||||
// Error response
|
||||
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||
@@ -488,23 +472,22 @@ namespace CryptoExchange.Net.Clients
|
||||
if (error.Code == null || error.Code == 0)
|
||||
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);
|
||||
if (typeof(T) == typeof(object))
|
||||
// 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)
|
||||
{
|
||||
// Invalid json
|
||||
var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception);
|
||||
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, valid.Error);
|
||||
}
|
||||
|
||||
// Json response received
|
||||
var parsedError = TryParseError(response.ResponseHeaders, accessor);
|
||||
var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
|
||||
if (parsedError != null)
|
||||
{
|
||||
if (parsedError is ServerRateLimitError rateError)
|
||||
@@ -517,30 +500,55 @@ namespace CryptoExchange.Net.Clients
|
||||
}
|
||||
|
||||
// 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>();
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), 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
|
||||
{
|
||||
// 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
|
||||
{
|
||||
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.
|
||||
/// If the response is an error this method should return the parsed error, else it should return null
|
||||
/// </summary>
|
||||
/// <param name="requestDefinition">Request definition</param>
|
||||
/// <param name="accessor">Data accessor</param>
|
||||
/// <param name="responseHeaders">The response headers</param>
|
||||
/// <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>
|
||||
/// 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>
|
||||
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>
|
||||
@@ -684,21 +693,21 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
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()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
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 (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, 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;
|
||||
@@ -727,7 +736,7 @@ namespace CryptoExchange.Net.Clients
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
@@ -82,6 +83,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
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 />
|
||||
public double IncomingKbps
|
||||
{
|
||||
@@ -221,7 +227,7 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// 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)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
@@ -260,11 +266,12 @@ namespace CryptoExchange.Net.Clients
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_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")));
|
||||
}
|
||||
|
||||
subscription.Status = SubscriptionStatus.Subscribing;
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
||||
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
|
||||
if (subQuery != null)
|
||||
{
|
||||
// Send the request and wait for answer
|
||||
@@ -273,7 +280,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
waitEvent?.Set();
|
||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||
if (isTimeout && subscription.Confirmed)
|
||||
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
|
||||
{
|
||||
// No response received, but the subscription did receive updates. We'll assume success
|
||||
}
|
||||
@@ -281,6 +288,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||
subscription.Status = SubscriptionStatus.Pending;
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
@@ -289,7 +297,7 @@ namespace CryptoExchange.Net.Clients
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
}
|
||||
|
||||
subscription.Confirmed = true;
|
||||
subscription.Status = SubscriptionStatus.Subscribed;
|
||||
if (ct != default)
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
@@ -308,11 +316,10 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||
/// </summary>
|
||||
/// <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="ct">Cancellation token</param>
|
||||
/// <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);
|
||||
}
|
||||
@@ -321,12 +328,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Send a query on a socket connection and wait for the response
|
||||
/// </summary>
|
||||
/// <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="query">The query</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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)
|
||||
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||
@@ -339,7 +345,7 @@ namespace CryptoExchange.Net.Clients
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<THandlerResponse>(default);
|
||||
|
||||
@@ -365,7 +371,7 @@ namespace CryptoExchange.Net.Clients
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
_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)
|
||||
@@ -490,25 +496,56 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="address">The address the socket is for</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="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>
|
||||
/// <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)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& s.Value.ApiClient.GetType() == GetType()
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
||||
&& s.Value.Connected);
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||
.Select(x => x.Value)
|
||||
.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)
|
||||
{
|
||||
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
|
||||
{
|
||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||
@@ -516,9 +553,12 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
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
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
@@ -712,7 +752,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
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)
|
||||
return socketResult.AsDataless();
|
||||
|
||||
@@ -809,9 +849,9 @@ namespace CryptoExchange.Net.Clients
|
||||
cs.SubscriptionStates.ForEach(subState =>
|
||||
{
|
||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
||||
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
||||
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)
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(stringValue))
|
||||
if (stringValue is null)
|
||||
return null;
|
||||
|
||||
if (!GetValue(enumType, stringValue!, out var result))
|
||||
if (!GetValue(enumType, stringValue, out var result))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
@@ -204,6 +204,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
// An empty/null value will always fail when parsing, so just return here
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
|
||||
@@ -60,13 +60,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
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));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
||||
return new CallResult<object>(new DeserializeError(info, ex));
|
||||
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,13 +86,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
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));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Unknown exception: {ex.Message}";
|
||||
return new CallResult<T>(new DeserializeError(info, ex));
|
||||
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +284,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
// Not a json message
|
||||
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
|
||||
IsValid = false;
|
||||
return new CallResult(new ServerError("Not a json value"));
|
||||
return new CallResult(new DeserializeError("Not a json value"));
|
||||
}
|
||||
|
||||
_document = JsonDocument.Parse(data);
|
||||
@@ -349,7 +347,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<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>
|
||||
<PackageVersion>9.2.0</PackageVersion>
|
||||
<AssemblyVersion>9.2.0</AssemblyVersion>
|
||||
<FileVersion>9.2.0</FileVersion>
|
||||
<PackageVersion>9.9.0</PackageVersion>
|
||||
<AssemblyVersion>9.9.0</AssemblyVersion>
|
||||
<FileVersion>9.9.0</FileVersion>
|
||||
<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>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
|
||||
@@ -88,8 +88,6 @@ namespace CryptoExchange.Net
|
||||
value -= offset;
|
||||
else value += (step.Value - offset);
|
||||
}
|
||||
|
||||
value = RoundDown(value, 8);
|
||||
|
||||
return value.Normalize();
|
||||
}
|
||||
@@ -348,22 +346,42 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
|
||||
// Infinity value should be parsed to min/max value
|
||||
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||
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>
|
||||
|
||||
@@ -498,8 +498,8 @@ namespace CryptoExchange.Net
|
||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
||||
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOrderBookSocketClient)client(x)!);
|
||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
||||
|
||||
@@ -17,22 +17,13 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
/// <summary>
|
||||
/// The identifiers for this processor
|
||||
/// The matcher for this listener
|
||||
/// </summary>
|
||||
public HashSet<string> ListenerIdentifiers { get; }
|
||||
public MessageMatcher MessageMatcher { get; }
|
||||
/// <summary>
|
||||
/// Handle a message
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <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);
|
||||
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matchedHandler);
|
||||
/// <summary>
|
||||
/// Deserialize a message into object of type
|
||||
/// </summary>
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
Uri Uri { get; }
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
Version HttpVersion { get; }
|
||||
/// <summary>
|
||||
/// internal request id for tracing
|
||||
/// </summary>
|
||||
int RequestId { get; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Create a request for an uri
|
||||
/// </summary>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <returns></returns>
|
||||
IRequest Create(HttpMethod method, Uri uri, int requestId);
|
||||
IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the requests created by this factory
|
||||
/// </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="proxy">Optional proxy to use when no http client is provided</param>
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
|
||||
void Configure(RestExchangeOptions options, HttpClient? httpClient = null);
|
||||
|
||||
/// <summary>
|
||||
/// Update settings
|
||||
/// </summary>
|
||||
/// <param name="proxy">Proxy 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
HttpStatusCode StatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http protocol version
|
||||
/// </summary>
|
||||
Version HttpVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the status code indicates a success status
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
@@ -43,5 +46,58 @@ namespace CryptoExchange.Net
|
||||
|
||||
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, Exception?> _reconnectingAllConnections;
|
||||
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()
|
||||
{
|
||||
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Warning,
|
||||
new EventId(3018, "AddRetryAfterGuard"),
|
||||
"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)
|
||||
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_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?> _failedToEvaluateMessage;
|
||||
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, string?, Exception?> _failedToDeserializeMessage;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _userMessageProcessingFailed;
|
||||
@@ -92,11 +92,6 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
new EventId(2009, "ErrorProcessingMessage"),
|
||||
"[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>(
|
||||
LogLevel.Warning,
|
||||
new EventId(2011, "ReceivedMessageNotRecognized"),
|
||||
@@ -190,7 +185,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Warning,
|
||||
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>(
|
||||
LogLevel.Warning,
|
||||
@@ -201,6 +196,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Trace,
|
||||
new EventId(2031, "SendingByteData"),
|
||||
"[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)
|
||||
@@ -256,9 +257,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_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)
|
||||
{
|
||||
|
||||
@@ -205,6 +205,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
public Version? HttpVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
@@ -251,6 +256,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? originalData,
|
||||
@@ -262,6 +268,7 @@ namespace CryptoExchange.Net.Objects
|
||||
Error? error) : base(error)
|
||||
{
|
||||
ResponseStatusCode = code;
|
||||
HttpVersion = httpVersion;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
RequestId = requestId;
|
||||
@@ -286,7 +293,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
@@ -297,7 +304,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
@@ -334,7 +341,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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 />
|
||||
@@ -355,6 +362,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP protocol version
|
||||
/// </summary>
|
||||
public Version? HttpVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
@@ -403,21 +415,9 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Create a new result
|
||||
/// </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(
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
@@ -431,6 +431,7 @@ namespace CryptoExchange.Net.Objects
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, originalData, error)
|
||||
{
|
||||
HttpVersion = httpVersion;
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
@@ -450,7 +451,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
/// Copy as a dataless result
|
||||
@@ -458,14 +459,14 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, 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>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
@@ -475,7 +476,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
@@ -486,7 +487,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
@@ -498,7 +499,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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>
|
||||
@@ -569,7 +570,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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 />
|
||||
|
||||
@@ -251,4 +251,47 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
DEX
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timeout behavior for queries
|
||||
/// </summary>
|
||||
public enum TimeoutBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Fail the request
|
||||
/// </summary>
|
||||
Fail,
|
||||
/// <summary>
|
||||
/// Mark the query as successful
|
||||
/// </summary>
|
||||
Succeed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscription status
|
||||
/// </summary>
|
||||
public enum SubscriptionStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Pending, waiting before (re)subscription can be started
|
||||
/// </summary>
|
||||
Pending,
|
||||
/// <summary>
|
||||
/// Currently (re)subscribing, will start producing updates soon if subscription is successful
|
||||
/// </summary>
|
||||
Subscribing,
|
||||
/// <summary>
|
||||
/// Subscribed and listening to updates
|
||||
/// </summary>
|
||||
Subscribed,
|
||||
/// <summary>
|
||||
/// Subscription is being closed and will stop producing updates
|
||||
/// </summary>
|
||||
Closing,
|
||||
/// <summary>
|
||||
/// Subscription is closed and will no long produce updates
|
||||
/// </summary>
|
||||
Closed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@@ -7,15 +8,52 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public abstract class Error
|
||||
{
|
||||
|
||||
private int? _code;
|
||||
/// <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>
|
||||
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>
|
||||
/// The message for the error that occurred
|
||||
/// The error code returned by the server
|
||||
/// </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>
|
||||
/// Underlying exception
|
||||
@@ -25,10 +63,13 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected Error (int? code, string message, Exception? exception)
|
||||
protected Error(string? errorCode, ErrorInfo errorInfo, Exception? exception)
|
||||
{
|
||||
Code = code;
|
||||
Message = message;
|
||||
ErrorCode = errorCode;
|
||||
ErrorType = errorInfo.ErrorType;
|
||||
Message = errorInfo.Message;
|
||||
ErrorDescription = errorInfo.ErrorDescription;
|
||||
IsTransient = errorInfo.IsTransient;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
@@ -38,7 +79,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { }
|
||||
public CantConnectError() : base(null, _errorInfo, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -69,14 +115,19 @@ namespace CryptoExchange.Net.Objects
|
||||
public class NoApiCredentialsError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -87,12 +138,19 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ServerError(string message) : base(null, message, null) { }
|
||||
public ServerError(ErrorInfo errorInfo, Exception? exception = null)
|
||||
: base(null, errorInfo, exception) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -101,14 +159,30 @@ namespace CryptoExchange.Net.Objects
|
||||
public class WebError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -117,30 +191,14 @@ namespace CryptoExchange.Net.Objects
|
||||
public class DeserializeError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, 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) { }
|
||||
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -149,14 +207,28 @@ namespace CryptoExchange.Net.Objects
|
||||
public class ArgumentError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info for missing parameter
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -172,7 +244,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||
protected BaseRateLimitError(ErrorInfo errorInfo, Exception? exception) : base(null, errorInfo, exception) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,15 +253,19 @@ namespace CryptoExchange.Net.Objects
|
||||
public class ClientRateLimitError : BaseRateLimitError
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
|
||||
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.RateLimitRequest, false, "Client rate limit exceeded");
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -198,14 +274,19 @@ namespace CryptoExchange.Net.Objects
|
||||
public class ServerRateLimitError : BaseRateLimitError
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -214,14 +295,19 @@ namespace CryptoExchange.Net.Objects
|
||||
public class CancellationRequestedError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </summary>
|
||||
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { }
|
||||
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.CancellationRequested, false, "Cancellation requested");
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </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>
|
||||
@@ -230,13 +316,18 @@ namespace CryptoExchange.Net.Objects
|
||||
public class InvalidOperationError : Error
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// Default error info
|
||||
/// </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>
|
||||
/// ctor
|
||||
/// </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 System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
@@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
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>
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
@@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.CachingEnabled = CachingEnabled;
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
item.HttpVersion = HttpVersion;
|
||||
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
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>
|
||||
/// Connection id
|
||||
/// </summary>
|
||||
@@ -76,6 +81,9 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
Path = path;
|
||||
Method = method;
|
||||
|
||||
if (!Path.StartsWith("/"))
|
||||
Path = $"/{Path}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="parameterPosition">Parameter position</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
HttpMethod method,
|
||||
@@ -57,8 +58,9 @@ namespace CryptoExchange.Net.Objects
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
|
||||
|
||||
/// <summary>
|
||||
/// 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="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
string identifier,
|
||||
@@ -86,7 +89,8 @@ namespace CryptoExchange.Net.Objects
|
||||
RequestBodyFormat? requestBodyFormat = null,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null)
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null)
|
||||
{
|
||||
|
||||
if (!_definitions.TryGetValue(identifier, out var def))
|
||||
@@ -100,7 +104,8 @@ namespace CryptoExchange.Net.Objects
|
||||
ArraySerialization = arraySerialization,
|
||||
RequestBodyFormat = requestBodyFormat,
|
||||
ParameterPosition = parameterPosition,
|
||||
PreventCaching = preventCaching ?? false
|
||||
PreventCaching = preventCaching ?? false,
|
||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
|
||||
};
|
||||
_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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
@@ -12,13 +14,27 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
private readonly SocketConnection _connection;
|
||||
private readonly Subscription _listener;
|
||||
|
||||
private object _eventLock = new object();
|
||||
private bool _connectionEventsSubscribed = true;
|
||||
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>
|
||||
/// Event when the status of the subscription changes
|
||||
/// </summary>
|
||||
public event Action<SubscriptionStatus>? SubscriptionStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
||||
/// </summary>
|
||||
public event Action ConnectionLost
|
||||
{
|
||||
add => _connection.ConnectionLost += value;
|
||||
remove => _connection.ConnectionLost -= value;
|
||||
add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -26,8 +42,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
add => _connection.ConnectionClosed += value;
|
||||
remove => _connection.ConnectionClosed -= value;
|
||||
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,8 +51,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action<Error> ResubscribingFailed
|
||||
{
|
||||
add => _connection.ResubscribingFailed += value;
|
||||
remove => _connection.ResubscribingFailed -= value;
|
||||
add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,8 +62,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored
|
||||
{
|
||||
add => _connection.ConnectionRestored += value;
|
||||
remove => _connection.ConnectionRestored -= value;
|
||||
add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,8 +71,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityPaused
|
||||
{
|
||||
add => _connection.ActivityPaused += value;
|
||||
remove => _connection.ActivityPaused -= value;
|
||||
add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,8 +80,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityUnpaused
|
||||
{
|
||||
add => _connection.ActivityUnpaused += value;
|
||||
remove => _connection.ActivityUnpaused -= value;
|
||||
add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,7 +111,128 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
||||
{
|
||||
_connection = connection;
|
||||
_connection.ConnectionClosed += HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost += HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused += HandlePausedEvent;
|
||||
_connection.ActivityUnpaused += HandleUnpausedEvent;
|
||||
|
||||
_listener = subscription;
|
||||
_listener.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
|
||||
}
|
||||
|
||||
private void UnsubscribeConnectionEvents()
|
||||
{
|
||||
lock (_eventLock)
|
||||
{
|
||||
if (!_connectionEventsSubscribed)
|
||||
return;
|
||||
|
||||
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost -= HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused -= HandlePausedEvent;
|
||||
_connection.ActivityUnpaused -= HandleUnpausedEvent;
|
||||
_connectionEventsSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosedEvent()
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
|
||||
// If we're not the subscription closing this connection don't bother emitting
|
||||
if (!_listener.IsClosingConnection)
|
||||
return;
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionClosedEventHandlers.ToList();
|
||||
|
||||
foreach(var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleConnectionLostEvent()
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionLostEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleConnectionRestoredEvent(TimeSpan period)
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action<TimeSpan>> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionRestoredEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback(period);
|
||||
}
|
||||
|
||||
private void HandleResubscribeFailedEvent(Error error)
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action<Error>> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _resubscribeFailedEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback(error);
|
||||
}
|
||||
|
||||
private void HandlePausedEvent()
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _activityPausedEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleUnpausedEvent()
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _activityUnpausedEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -549,7 +550,7 @@ namespace CryptoExchange.Net.OrderBook
|
||||
return new CallResult<bool>(new CancellationRequestedError());
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => _request.RequestUri!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version HttpVersion => _request.Version!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int RequestId { get; }
|
||||
|
||||
@@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Requests
|
||||
{
|
||||
@@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
||||
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
||||
{
|
||||
if (client == null)
|
||||
client = CreateClient(proxy, requestTimeout);
|
||||
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
|
||||
|
||||
_httpClient = client;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
|
||||
{
|
||||
if (_httpClient == null)
|
||||
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 />
|
||||
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();
|
||||
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 handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
|
||||
var client = new HttpClient(handler)
|
||||
{
|
||||
Timeout = requestTimeout
|
||||
Timeout = requestTimeout
|
||||
};
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <inheritdoc />
|
||||
public HttpStatusCode StatusCode => _response.StatusCode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version HttpVersion => _response.Version;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
/// <summary>
|
||||
/// Quantity can be either base or quote quantity
|
||||
/// </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,
|
||||
INextPageToken? nextPageToken = null) :
|
||||
base(result.ResponseStatusCode,
|
||||
result.HttpVersion,
|
||||
result.ResponseHeaders,
|
||||
result.ResponseTime,
|
||||
result.ResponseLength,
|
||||
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
WebCallResult<T> result,
|
||||
INextPageToken? nextPageToken = null) :
|
||||
base(result.ResponseStatusCode,
|
||||
result.HttpVersion,
|
||||
result.ResponseHeaders,
|
||||
result.ResponseTime,
|
||||
result.ResponseLength,
|
||||
@@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
string exchange,
|
||||
TradingMode[]? dataTradeModes,
|
||||
HttpStatusCode? code,
|
||||
Version? httpVersion,
|
||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
@@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
Error? error,
|
||||
INextPageToken? nextPageToken = null) : base(
|
||||
code,
|
||||
httpVersion,
|
||||
responseHeaders,
|
||||
responseTime,
|
||||
responseLength,
|
||||
@@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <returns></returns>
|
||||
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 />
|
||||
|
||||
@@ -58,19 +58,19 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public virtual Error? ValidateRequest(string exchange, ExchangeParameters? exchangeParameters, TradingMode? tradingMode, TradingMode[] supportedTradingModes)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(param.Name))
|
||||
{
|
||||
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
|
||||
{
|
||||
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>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -131,12 +140,25 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (!string.IsNullOrEmpty(param.Name))
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -67,23 +67,23 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, GetKlinesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
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))
|
||||
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)
|
||||
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 (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.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;
|
||||
|
||||
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)
|
||||
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))
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public Error? Validate(GetRecentTradesRequest request)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -36,16 +36,16 @@ namespace CryptoExchange.Net.SharedApis
|
||||
SharedQuantitySupport quantitySupport)
|
||||
{
|
||||
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)
|
||||
throw new ArgumentException("OrderType can't be `Other`", nameof(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))
|
||||
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);
|
||||
if (quantityError != null)
|
||||
|
||||
@@ -34,10 +34,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
throw new ArgumentException("OrderType can't be `Other`", nameof(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))
|
||||
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);
|
||||
if (quantityError != null)
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, SubscribeKlineRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public override Error? ValidateRequest(string exchange, SubscribeOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (supportedType == quantityType)
|
||||
return true;
|
||||
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
|
||||
return true;
|
||||
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset))
|
||||
return true;
|
||||
|
||||
@@ -77,20 +80,20 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity)
|
||||
{
|
||||
var supportedType = GetSupportedQuantityType(side, type);
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAsset)
|
||||
if (supportedType == SharedQuantityType.BaseAndQuoteAsset || supportedType == SharedQuantityType.BaseAndQuoteAssetAndContracts)
|
||||
return 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)
|
||||
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)
|
||||
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)
|
||||
return new ArgumentError($"Quantity for {side}.{type} required in contracts");
|
||||
return ArgumentError.Invalid("Quantity", $"Quantity for {side}.{type} required in contracts");
|
||||
|
||||
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>
|
||||
/// Symbol request
|
||||
/// </summary>
|
||||
public record SharedSymbolRequest : SharedRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Trading mode
|
||||
/// </summary>
|
||||
public TradingMode TradingMode { get; }
|
||||
/// <summary>
|
||||
/// The symbol
|
||||
/// </summary>
|
||||
public SharedSymbol Symbol { get; set; }
|
||||
public SharedSymbol? Symbol { get; set; }
|
||||
/// <summary>
|
||||
/// Symbols
|
||||
/// </summary>
|
||||
public SharedSymbol[]? Symbols { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -16,6 +29,22 @@
|
||||
public SharedSymbolRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
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>
|
||||
/// Request to subscribe to book ticker updates
|
||||
@@ -13,5 +15,22 @@
|
||||
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>
|
||||
/// Request to subscribe to kline/candlestick updates
|
||||
@@ -20,5 +22,26 @@
|
||||
{
|
||||
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>
|
||||
/// Request to subscribe to order book snapshot updates
|
||||
@@ -20,5 +22,22 @@
|
||||
{
|
||||
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>
|
||||
/// Request to subscribe to ticker updates
|
||||
@@ -13,5 +16,22 @@
|
||||
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>
|
||||
/// Request to subscribe to trade updates
|
||||
@@ -13,5 +15,22 @@
|
||||
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
|
||||
/// </summary>
|
||||
public int? MinConfirmations { get; set; }
|
||||
/// <summary>
|
||||
/// The contract address
|
||||
/// </summary>
|
||||
public string? ContractAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override SharedSymbol SharedSymbol => new SharedSymbol(TradingMode, BaseAsset.ToUpperInvariant(), QuoteAsset.ToUpperInvariant(), DeliveryTime) { SymbolName = Name };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public record SharedPosition : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Position id
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
/// <summary>
|
||||
/// Current size of the position
|
||||
/// </summary>
|
||||
|
||||
@@ -69,5 +69,11 @@
|
||||
Name = symbol;
|
||||
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.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -252,6 +253,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -28,6 +29,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public TimeSpan? RequestTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What should happen if the query times out
|
||||
/// </summary>
|
||||
public TimeoutBehavior TimeoutBehavior { get; set; } = TimeoutBehavior.Fail;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
@@ -60,9 +66,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
public AsyncResetEvent? ContinueAwaiter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Strings to match this query to a received message
|
||||
/// Matcher for this query
|
||||
/// </summary>
|
||||
public abstract HashSet<string> ListenerIdentifiers { get; set; }
|
||||
public MessageMatcher MessageMatcher { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The query request object
|
||||
@@ -84,13 +90,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
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>
|
||||
/// Wait event for response
|
||||
/// </summary>
|
||||
@@ -161,23 +160,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Handle a response message
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
||||
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query
|
||||
/// </summary>
|
||||
/// <typeparam name="TServerResponse">The type returned from the server</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>
|
||||
/// The typed call result
|
||||
/// </summary>
|
||||
@@ -194,25 +186,22 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <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 (!ValidateMessage(typedMessage))
|
||||
if (!PreCheckMessage(connection, message))
|
||||
return CallResult.SuccessResult;
|
||||
|
||||
CurrentResponses++;
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
Completed = true;
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
Response = message.Data;
|
||||
}
|
||||
|
||||
if (Result?.Success != false)
|
||||
// If an error result is already set don't override that
|
||||
Result = HandleMessage(connection, typedMessage);
|
||||
Result = check.Handle(connection, message);
|
||||
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
Completed = true;
|
||||
_event.Set();
|
||||
if (ContinueAwaiter != null)
|
||||
await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
|
||||
@@ -224,26 +213,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Validate if a message is actually processable by this query
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <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);
|
||||
public virtual bool PreCheckMessage(SocketConnection connection, DataEvent<object> message) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Timeout()
|
||||
{
|
||||
if (Completed)
|
||||
return;
|
||||
|
||||
|
||||
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();
|
||||
_event.Set();
|
||||
}
|
||||
@@ -257,29 +240,4 @@ namespace CryptoExchange.Net.Sockets
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ using System.Diagnostics;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
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 readonly object _listenersLock;
|
||||
private readonly List<IMessageProcessor> _listeners;
|
||||
@@ -229,6 +239,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
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>
|
||||
/// New socket connection
|
||||
/// </summary>
|
||||
@@ -281,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
lock (_listenersLock)
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
|
||||
subscription.Reset();
|
||||
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
@@ -446,9 +461,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Handle a message
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
@@ -475,9 +487,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -485,7 +497,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
var listenId = ApiClient.GetListenerIdentifier(accessor);
|
||||
if (listenId == null)
|
||||
{
|
||||
originalData = outputOriginalData ? accessor.GetOriginalString() : "[OutputOriginalData is false]";
|
||||
originalData ??= "[OutputOriginalData is false]";
|
||||
if (!ApiClient.UnhandledMessageExpected)
|
||||
_logger.FailedToEvaluateMessage(SocketId, originalData);
|
||||
|
||||
@@ -493,18 +505,81 @@ namespace CryptoExchange.Net.Sockets
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Get the listeners interested in this message
|
||||
List<IMessageProcessor> processors;
|
||||
lock (_listenersLock)
|
||||
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
|
||||
bool processed = false;
|
||||
var totalUserTime = 0;
|
||||
|
||||
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.Status == SubscriptionStatus.Subscribing)
|
||||
{
|
||||
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||
subscriptionProcessor.Status = SubscriptionStatus.Subscribed;
|
||||
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)
|
||||
{
|
||||
List<string> listenerIds;
|
||||
lock (_listenersLock)
|
||||
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
|
||||
listenerIds = _listeners.Select(l => l.MessageMatcher.ToString()).ToList();
|
||||
|
||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
||||
UnhandledMessage?.Invoke(accessor);
|
||||
}
|
||||
@@ -512,69 +587,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deserializationCache.Clear();
|
||||
accessor.Clear();
|
||||
}
|
||||
}
|
||||
@@ -640,13 +657,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
public async Task CloseAsync(Subscription subscription)
|
||||
{
|
||||
// If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues
|
||||
while (subscription.IsResubscribing)
|
||||
while (subscription.Status == SubscriptionStatus.Subscribing)
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
|
||||
subscription.Closed = true;
|
||||
subscription.Status = SubscriptionStatus.Closing;
|
||||
|
||||
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||
{
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.ClosingSubscription(SocketId, subscription.Id);
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
@@ -654,11 +674,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
bool anyDuplicateSubscription;
|
||||
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;
|
||||
lock (_listenersLock)
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
{
|
||||
@@ -676,6 +696,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (Status == SocketStatus.Closing)
|
||||
{
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
_logger.AlreadyClosing(SocketId);
|
||||
return;
|
||||
}
|
||||
@@ -683,12 +704,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (shouldCloseConnection)
|
||||
{
|
||||
Status = SocketStatus.Closing;
|
||||
subscription.IsClosingConnection = true;
|
||||
_logger.ClosingNoMoreSubscriptions(SocketId);
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
lock (_listenersLock)
|
||||
_listeners.Remove(subscription);
|
||||
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -763,22 +787,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||
{
|
||||
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
||||
return query.Result ?? new CallResult(new TimeoutError());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a query request and wait for an answer
|
||||
/// </summary>
|
||||
/// <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="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <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);
|
||||
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)
|
||||
@@ -863,7 +886,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
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);
|
||||
return new CallResult(new InvalidOperationError(info));
|
||||
}
|
||||
@@ -898,7 +921,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
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);
|
||||
return new CallResult(new InvalidOperationError(info));
|
||||
}
|
||||
@@ -973,7 +996,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
|
||||
if (subList.Count == 0)
|
||||
break;
|
||||
@@ -982,34 +1005,32 @@ namespace CryptoExchange.Net.Sockets
|
||||
foreach (var subscription in subList)
|
||||
{
|
||||
subscription.ConnectionInvocations = 0;
|
||||
if (subscription.Closed)
|
||||
if (!subscription.Active)
|
||||
// Can be closed during resubscribing
|
||||
continue;
|
||||
|
||||
subscription.IsResubscribing = true;
|
||||
subscription.Status = SubscriptionStatus.Subscribing;
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.Status = SubscriptionStatus.Pending;
|
||||
return result;
|
||||
}
|
||||
|
||||
var subQuery = subscription.GetSubQuery(this);
|
||||
var subQuery = subscription.CreateSubscriptionQuery(this);
|
||||
if (subQuery == null)
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.Status = SubscriptionStatus.Subscribed;
|
||||
continue;
|
||||
}
|
||||
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.Status = r.Result.Success ? SubscriptionStatus.Subscribed: SubscriptionStatus.Pending;
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
waitEvent.Set();
|
||||
if (r.Result.Success)
|
||||
subscription.Confirmed = true;
|
||||
return r.Result;
|
||||
}));
|
||||
}
|
||||
@@ -1030,7 +1051,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
internal async Task UnsubscribeAsync(Subscription subscription)
|
||||
{
|
||||
var unsubscribeRequest = subscription.GetUnsubQuery();
|
||||
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
|
||||
if (unsubscribeRequest == null)
|
||||
return;
|
||||
|
||||
@@ -1043,7 +1064,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket is not connected"));
|
||||
|
||||
var subQuery = subscription.GetSubQuery(this);
|
||||
var subQuery = subscription.CreateSubscriptionQuery(this);
|
||||
if (subQuery == null)
|
||||
return CallResult.SuccessResult;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -33,21 +34,33 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Is it a user subscription
|
||||
/// </summary>
|
||||
public bool UserSubscription { get; set; }
|
||||
|
||||
|
||||
private SubscriptionStatus _status;
|
||||
/// <summary>
|
||||
/// Has the subscription been confirmed
|
||||
/// Current subscription status
|
||||
/// </summary>
|
||||
public bool Confirmed { get; set; }
|
||||
public SubscriptionStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (_status == value)
|
||||
return;
|
||||
|
||||
_status = value;
|
||||
Task.Run(() => StatusChanged?.Invoke(value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription closed
|
||||
/// Whether the subscription is active
|
||||
/// </summary>
|
||||
public bool Closed { get; set; }
|
||||
public bool Active => Status != SubscriptionStatus.Closing && Status != SubscriptionStatus.Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription currently resubscribing
|
||||
/// Whether the unsubscribing of this subscription lead to the closing of the connection
|
||||
/// </summary>
|
||||
public bool IsResubscribing { get; set; }
|
||||
public bool IsClosingConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
@@ -60,9 +73,9 @@ namespace CryptoExchange.Net.Sockets
|
||||
public bool Authenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Strings to match this subscription to a received message
|
||||
/// Matcher for this subscription
|
||||
/// </summary>
|
||||
public abstract HashSet<string> ListenerIdentifiers { get; set; }
|
||||
public MessageMatcher MessageMatcher { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token registration
|
||||
@@ -73,25 +86,29 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Exception event
|
||||
/// </summary>
|
||||
public event Action<Exception>? Exception;
|
||||
|
||||
/// <summary>
|
||||
/// Get the deserialization type for this message
|
||||
/// Listener unsubscribed event
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
public abstract Type? GetMessageType(IMessageAccessor message);
|
||||
public event Action<SubscriptionStatus>? StatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Subscription topic
|
||||
/// </summary>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="authenticated"></param>
|
||||
/// <param name="userSubscription"></param>
|
||||
public Subscription(ILogger logger, bool authenticated, bool userSubscription = true)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -100,11 +117,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
Id = ExchangeHelpers.NextId();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new subscription query
|
||||
/// </summary>
|
||||
public Query? CreateSubscriptionQuery(SocketConnection connection)
|
||||
{
|
||||
var query = GetSubQuery(connection);
|
||||
SubscriptionQuery = query;
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the subscribe query to send when subscribing
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract Query? GetSubQuery(SocketConnection connection);
|
||||
protected abstract Query? GetSubQuery(SocketConnection connection);
|
||||
|
||||
/// <summary>
|
||||
/// Handle a subscription query response
|
||||
@@ -118,11 +145,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="message"></param>
|
||||
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>
|
||||
/// Get the unsubscribe query to send when unsubscribing
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract Query? GetUnsubQuery();
|
||||
protected abstract Query? GetUnsubQuery(SocketConnection connection);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
||||
@@ -130,14 +167,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// Handle an update message
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink matcher)
|
||||
{
|
||||
ConnectionInvocations++;
|
||||
TotalInvocations++;
|
||||
return Task.FromResult(DoHandleMessage(connection, message));
|
||||
return Task.FromResult(matcher.Handle(connection, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,7 +179,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
Confirmed = false;
|
||||
Status = SubscriptionStatus.Pending;
|
||||
DoHandleReset();
|
||||
}
|
||||
|
||||
@@ -154,14 +188,6 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
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>
|
||||
/// Invoke the exception event
|
||||
/// </summary>
|
||||
@@ -175,14 +201,14 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// State of this subscription
|
||||
/// </summary>
|
||||
/// <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="Status">Subscription status</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(
|
||||
int Id,
|
||||
bool Confirmed,
|
||||
SubscriptionStatus Status,
|
||||
int Invocations,
|
||||
HashSet<string> Identifiers
|
||||
MessageMatcher ListenMatcher
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@@ -191,7 +217,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public SubscriptionState GetState()
|
||||
{
|
||||
return new SubscriptionState(Id, Confirmed, TotalInvocations, ListenerIdentifiers);
|
||||
return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,41 +18,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="authenticated"></param>
|
||||
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
|
||||
{
|
||||
Confirmed = true;
|
||||
Status = SubscriptionStatus.Subscribed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Query? GetSubQuery(SocketConnection connection) => null;
|
||||
protected override Query? GetSubQuery(SocketConnection connection) => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Query? GetUnsubQuery() => 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);
|
||||
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
var stringValue = jsonValue.GetString();
|
||||
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}");
|
||||
}
|
||||
else if (objectValue is DateTime time)
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
public Version HttpVersion { 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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -14,11 +15,11 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
_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.Uri = uri;
|
||||
@@ -26,6 +27,6 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
private readonly Stream _response;
|
||||
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
public Version HttpVersion { get; }
|
||||
|
||||
public bool IsSuccessStatusCode { get; }
|
||||
|
||||
@@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
public TestResponse(HttpStatusCode code, Stream response)
|
||||
{
|
||||
StatusCode = code;
|
||||
HttpVersion = new Version(2, 0);
|
||||
IsSuccessStatusCode = code == HttpStatusCode.OK;
|
||||
_response = response;
|
||||
}
|
||||
|
||||
@@ -128,23 +128,27 @@ namespace CryptoExchange.Net.Testing
|
||||
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? 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.AuthenticateRequest(
|
||||
client,
|
||||
new Uri(host.AppendPath(path)),
|
||||
method,
|
||||
ref uriParams,
|
||||
ref bodyParams,
|
||||
ref headers,
|
||||
true,
|
||||
client.ArraySerialization,
|
||||
client.ParameterPositions[method],
|
||||
client.RequestBodyFormat
|
||||
authProvider.ProcessRequest(
|
||||
client,
|
||||
requestDefinition
|
||||
);
|
||||
|
||||
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))
|
||||
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
|
||||
|
||||
@@ -176,18 +176,32 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.KlineTrackerStarting(SymbolName);
|
||||
|
||||
var startResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!startResult)
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
|
||||
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;
|
||||
return new CallResult(startResult.Error!);
|
||||
return subResult;
|
||||
}
|
||||
|
||||
_updateSubscription = startResult.Data;
|
||||
_updateSubscription = subResult.Data;
|
||||
_updateSubscription.ConnectionLost += HandleConnectionLost;
|
||||
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_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;
|
||||
_logger.KlineTrackerStarted(SymbolName);
|
||||
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
|
||||
/// </summary>
|
||||
/// <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)
|
||||
return subResult;
|
||||
return CallResult.SuccessResult;
|
||||
|
||||
var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value);
|
||||
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))
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(result.Error!);
|
||||
}
|
||||
return result;
|
||||
|
||||
if (Limit != null && data.Count > Limit)
|
||||
break;
|
||||
@@ -249,7 +247,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
}
|
||||
|
||||
SetInitialData(data);
|
||||
return subResult;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -199,10 +199,15 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
_startWithSnapshot = startWithSnapshot;
|
||||
Status = SyncStatus.Syncing;
|
||||
_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)
|
||||
{
|
||||
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception);
|
||||
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message ?? subResult.Error!.ErrorDescription!, subResult.Error.Exception);
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
@@ -211,6 +216,15 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
_updateSubscription.ConnectionLost += HandleConnectionLost;
|
||||
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
|
||||
|
||||
var result = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return result;
|
||||
}
|
||||
|
||||
SetSyncStatus();
|
||||
_logger.TradeTrackerStarted(SymbolName);
|
||||
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
|
||||
/// </summary>
|
||||
/// <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)
|
||||
return subResult;
|
||||
return CallResult.SuccessResult;
|
||||
|
||||
if (_historyRestClient != null)
|
||||
{
|
||||
@@ -256,12 +258,8 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(result.Error!);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
if (Limit != null && data.Count > Limit)
|
||||
break;
|
||||
|
||||
@@ -279,15 +277,13 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false);
|
||||
if (!snapshot)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(snapshot.Error!);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
SetInitialData(snapshot.Data);
|
||||
}
|
||||
|
||||
return subResult;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
|
||||
@@ -5,28 +5,31 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="11.1.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.1.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.1.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.1.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.1.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.1.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="2.1.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.1.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="2.1.1" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.1.0" />
|
||||
<PackageReference Include="Binance.Net" Version="11.8.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.8.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.9.0" />
|
||||
<PackageReference Include="BloFin.Net" Version="1.1.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.9.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.8.0" />
|
||||
<PackageReference Include="CoinW.Net" Version="1.5.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.9.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="2.8.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.9.1" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="2.13.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.9.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.8.0" />
|
||||
<PackageReference Include="Jkorf.Aster.Net" Version="1.0.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.8.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.8.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.8.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="1.0.1" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="2.1.0" />
|
||||
<PackageReference Include="XT.Net" Version="2.1.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="1.7.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="2.9.0" />
|
||||
<PackageReference Include="XT.Net" Version="2.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
@page "/"
|
||||
@inject IAsterRestClient asterClient
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBingXRestClient bingXClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBitgetRestClient bitgetClient
|
||||
@inject IBitMartRestClient bitmartClient
|
||||
@inject IBitMEXRestClient bitmexClient
|
||||
@inject IBloFinRestClient bloFinClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinbaseRestClient coinbaseClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject ICoinWRestClient coinWClient
|
||||
@inject ICryptoComRestClient cryptocomClient
|
||||
@inject IDeepCoinRestClient deepCoinClient
|
||||
@inject IGateIoRestClient gateioClient
|
||||
@@ -32,20 +35,23 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var asterTask = asterClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_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 coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
|
||||
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
|
||||
var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync();
|
||||
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
|
||||
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||
var htxTask = htxClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync(); // HyperLiquid does not have BTC spot trading
|
||||
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync();
|
||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
@@ -54,7 +60,10 @@
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
|
||||
|
||||
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
|
||||
if (asterTask.Result.Success)
|
||||
_prices.Add("Aster", asterTask.Result.Data.LastPrice);
|
||||
|
||||
if (binanceTask.Result.Success)
|
||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||
@@ -74,6 +83,9 @@
|
||||
if (bitmexTask.Result.Success)
|
||||
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
|
||||
|
||||
if (bloFinTask.Result.Success)
|
||||
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
|
||||
|
||||
if (bybitTask.Result.Success)
|
||||
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
||||
|
||||
@@ -83,6 +95,9 @@
|
||||
if (coinexTask.Result.Success)
|
||||
_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)
|
||||
_prices.Add("CryptoCom", cryptocomTask.Result.Data.First().LastPrice ?? 0);
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
@page "/LiveData"
|
||||
@inject IAsterSocketClient asterSocketClient
|
||||
@inject IBinanceSocketClient binanceSocketClient
|
||||
@inject IBingXSocketClient bingXSocketClient
|
||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||
@inject IBitgetSocketClient bitgetSocketClient
|
||||
@inject IBitMartSocketClient bitmartSocketClient
|
||||
@inject IBitMEXSocketClient bitmexSocketClient
|
||||
@inject IBloFinSocketClient bloFinSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinbaseSocketClient coinbaseSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@inject ICoinWSocketClient coinWSocketClient
|
||||
@inject ICryptoComSocketClient cryptocomSocketClient
|
||||
@inject IDeepCoinSocketClient deepCoinSocketClient
|
||||
@inject IGateIoSocketClient gateioSocketClient
|
||||
@@ -41,14 +44,19 @@
|
||||
{
|
||||
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
||||
{
|
||||
// Aster doesn't support the ETH/BTC pair
|
||||
//asterSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Aster", data.Data.LastPrice)),
|
||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", 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)),
|
||||
// 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)),
|
||||
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)),
|
||||
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
|
||||
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
|
||||
@@ -59,7 +67,8 @@
|
||||
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
|
||||
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)),
|
||||
// Toobit doesn't support the ETH/BTC pair
|
||||
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
@page "/OrderBooks"
|
||||
@using System.Collections.Concurrent
|
||||
@using System.Timers
|
||||
@using Aster.Net.Interfaces
|
||||
@using Binance.Net.Interfaces
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using BitMEX.Net.Interfaces;
|
||||
@using BloFin.Net.Interfaces;
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using CoinW.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Authentication
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@@ -25,15 +28,18 @@
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IAsterOrderBookFactory asterFactory
|
||||
@inject IBinanceOrderBookFactory binanceFactory
|
||||
@inject IBingXOrderBookFactory bingXFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@inject IBitgetOrderBookFactory bitgetFactory
|
||||
@inject IBitMartOrderBookFactory bitmartFactory
|
||||
@inject IBitMEXOrderBookFactory bitmexFactory
|
||||
@inject IBloFinOrderBookFactory bloFinFactory
|
||||
@inject IBybitOrderBookFactory bybitFactory
|
||||
@inject ICoinbaseOrderBookFactory coinbaseFactory
|
||||
@inject ICoinExOrderBookFactory coinExFactory
|
||||
@inject ICoinWOrderBookFactory coinWFactory
|
||||
@inject ICryptoComOrderBookFactory cryptocomFactory
|
||||
@inject IDeepCoinOrderBookFactory deepCoinFactory
|
||||
@inject IGateIoOrderBookFactory gateioFactory
|
||||
@@ -53,7 +59,7 @@
|
||||
@foreach(var book in _books.OrderBy(p => p.Key))
|
||||
{
|
||||
<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)
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
@@ -79,28 +85,28 @@
|
||||
|
||||
_books = new Dictionary<string, ISymbolOrderBook>
|
||||
{
|
||||
{ "Aster", binanceFactory.CreateSpot("ETHUSDT") },
|
||||
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
|
||||
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
|
||||
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
|
||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||
// DeepCoin does not support the ETH/BTC pair
|
||||
//{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
|
||||
{ "DeepCoin", deepCoinFactory.Create("ETH-USDT") },
|
||||
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
||||
// HyperLiquid does not support the ETH/BTC pair
|
||||
//{ "HyperLiquid", hyperLiquidFactory.Create("ETH/BTC") },
|
||||
{ "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
// Toobit does not support the ETH/BTC pair
|
||||
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
|
||||
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
|
||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||
{ "XT", xtFactory.CreateSpot("eth_btc") },
|
||||
};
|
||||
@@ -115,7 +121,7 @@
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer?.Stop();
|
||||
_timer.Dispose();
|
||||
foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
|
||||
// It's not necessary to wait for this
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
@page "/Trackers"
|
||||
@using System.Collections.Concurrent
|
||||
@using System.Timers
|
||||
@using Aster.Net.Interfaces
|
||||
@using Binance.Net.Interfaces
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMEX.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using BloFin.Net.Interfaces
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using CoinW.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using CryptoCom.Net.Interfaces
|
||||
@@ -26,15 +29,18 @@
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IAsterTrackerFactory asterFactory
|
||||
@inject IBinanceTrackerFactory binanceFactory
|
||||
@inject IBingXTrackerFactory bingXFactory
|
||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||
@inject IBitgetTrackerFactory bitgetFactory
|
||||
@inject IBitMartTrackerFactory bitmartFactory
|
||||
@inject IBitMEXTrackerFactory bitmexFactory
|
||||
@inject IBloFinTrackerFactory bloFinFactory
|
||||
@inject IBybitTrackerFactory bybitFactory
|
||||
@inject ICoinbaseTrackerFactory coinbaseFactory
|
||||
@inject ICoinExTrackerFactory coinExFactory
|
||||
@inject ICoinWTrackerFactory coinWFactory
|
||||
@inject ICryptoComTrackerFactory cryptocomFactory
|
||||
@inject IDeepCoinTrackerFactory deepCoinFactory
|
||||
@inject IGateIoTrackerFactory gateioFactory
|
||||
@@ -69,32 +75,36 @@
|
||||
|
||||
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>
|
||||
{
|
||||
{ binanceFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bingXFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ htxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
// HyperLiquid doesn't support spot pair, but does have a futures BTC/USDC pair
|
||||
{ hyperLiquidFactory.CreateTradeTracker(new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDC"), period: TimeSpan.FromMinutes(5)) },
|
||||
{ krakenFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ asterFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bloFinFactory.CreateTradeTracker(usdtPerpLinSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinWFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ deepCoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ gateioFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ htxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ hyperLiquidFactory.CreateTradeTracker(usdcSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ krakenFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ kucoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ toobitFactory.CreateTradeTracker(usdtSpotSymbol, 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()));
|
||||
@@ -121,8 +131,8 @@
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
_timer?.Stop();
|
||||
_timer?.Dispose();
|
||||
foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected))
|
||||
// It's not necessary to wait for this
|
||||
_ = tracker.StopAsync();
|
||||
|
||||
@@ -33,14 +33,17 @@ namespace BlazorClient
|
||||
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
});
|
||||
|
||||
services.AddAster();
|
||||
services.AddBingX();
|
||||
services.AddBitfinex();
|
||||
services.AddBitget();
|
||||
services.AddBitMart();
|
||||
services.AddBitMEX();
|
||||
services.AddBloFin();
|
||||
services.AddBybit();
|
||||
services.AddCoinbase();
|
||||
services.AddCoinEx();
|
||||
services.AddCoinW();
|
||||
services.AddCryptoCom();
|
||||
services.AddDeepCoin();
|
||||
services.AddGateIo();
|
||||
|
||||
@@ -8,15 +8,18 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazorClient
|
||||
@using BlazorClient.Shared
|
||||
@using Aster.Net.Interfaces.Clients;
|
||||
@using Binance.Net.Interfaces.Clients;
|
||||
@using BingX.Net.Interfaces.Clients;
|
||||
@using Bitfinex.Net.Interfaces.Clients;
|
||||
@using Bitget.Net.Interfaces.Clients;
|
||||
@using BitMart.Net.Interfaces.Clients;
|
||||
@using BitMEX.Net.Interfaces.Clients;
|
||||
@using BloFin.Net.Interfaces.Clients;
|
||||
@using Bybit.Net.Interfaces.Clients;
|
||||
@using Coinbase.Net.Interfaces.Clients;
|
||||
@using CoinEx.Net.Interfaces.Clients;
|
||||
@using CoinW.Net.Interfaces.Clients;
|
||||
@using CryptoCom.Net.Interfaces.Clients;
|
||||
@using DeepCoin.Net.Interfaces.Clients;
|
||||
@using GateIo.Net.Interfaces.Clients;
|
||||
|
||||
@@ -12,15 +12,18 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
||||
|
||||
||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|
||||
|--|--|--|--|--|--|--|
|
||||
||Aster|DEX|[JKorf/Aster.Net](https://github.com/JKorf/Aster.Net)|[](https://www.nuget.org/packages/JKorf.Aster.Net)|[Link](https://www.asterdex.com/en/referral/FD2E11)|4%|
|
||||
||Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%|
|
||||
||BingX|CEX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[](https://www.nuget.org/packages/JK.BingX.Net)|[Link](https://bingx.com/invite/FFHRJKWG/)|20%|
|
||||
||Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[](https://www.nuget.org/packages/Bitfinex.Net)|-|-|
|
||||
||Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|
||||
||BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|
||||
||BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|
||||
||BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[](https://www.nuget.org/packages/BloFin.Net)|-|-|
|
||||
||Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|
||||
||Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|
||||
||CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
|
||||
||CoinW|CEX|[JKorf/CoinW.Net](https://github.com/JKorf/CoinW.Net)|[](https://www.nuget.org/packages/CoinW.Net)|[Link](https://www.coinw.com/register?rc=rbtnp)|-|
|
||||
||CoinGecko|-|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|-|-|
|
||||
||Crypto.com|CEX|[JKorf/CryptoCom.Net](https://github.com/JKorf/CryptoCom.Net)|[](https://www.nuget.org/packages/CryptoCom.Net)|[Link](https://crypto.com/exch/26ge92xbkn)|-|
|
||||
||DeepCoin|CEX|[JKorf/DeepCoin.Net](https://github.com/JKorf/DeepCoin.Net)|[](https://www.nuget.org/packages/DeepCoin.Net)|[Link](https://s.deepcoin.com/jddhfca)|-|
|
||||
@@ -37,6 +40,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.
|
||||
|
||||
### 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
|
||||
[](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.
|
||||
@@ -58,6 +65,57 @@ 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).
|
||||
|
||||
## Release notes
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated socket Subscription status handling
|
||||
* Added SubscriptionStatusChanged event to UpdateSubscription (SubscribeAsync methods reponse)
|
||||
* Fixed timing issue for connection events in UpdateSubscription
|
||||
|
||||
* 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
|
||||
* Added support for sending byte data on websocket
|
||||
* Added support for handling both string and byte data with different IMessageAccessor types
|
||||
|
||||
Reference in New Issue
Block a user