mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 00:43:03 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b43d2a2040 | |||
| ba9c406def | |||
| f5f4d50cc9 | |||
| f87506b490 | |||
| f6f9a53ce5 | |||
| 61130ef54e | |||
| e8bcbd59be | |||
| 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 |
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||||
<PackageVersion>9.5.0</PackageVersion>
|
<PackageVersion>9.10.0</PackageVersion>
|
||||||
<AssemblyVersion>9.5.0</AssemblyVersion>
|
<AssemblyVersion>9.10.0</AssemblyVersion>
|
||||||
<FileVersion>9.5.0</FileVersion>
|
<FileVersion>9.10.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CryptoExchange.Net" Version="9.5.0" />
|
<PackageReference Include="CryptoExchange.Net" Version="9.10.0" />
|
||||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -5,6 +5,21 @@
|
|||||||
Protobuf support for CryptoExchange.Net.
|
Protobuf support for CryptoExchange.Net.
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 9.10.0 - 15 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* 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
|
* Version 9.5.0 - 19 Aug 2025
|
||||||
* Updated CryptoExchange.Net version to 9.5.0
|
* Updated CryptoExchange.Net version to 9.5.0
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using NUnit.Framework.Legacy;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -113,6 +114,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
|
HttpVersion.Version11,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
@@ -143,6 +145,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
|
HttpVersion.Version11,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||||
|
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase()]
|
[TestCase()]
|
||||||
@@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
protected 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 GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
||||||
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -69,8 +69,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -90,12 +90,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,13 +118,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
RequestFactory.Configure(options, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -239,7 +239,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
additionalHeaders);
|
additionalHeaders);
|
||||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||||
TotalRequestsMade++;
|
TotalRequestsMade++;
|
||||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
var result = await GetResponseAsync<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||||
if (result.Error is not CancellationRequestedError)
|
if (result.Error is not CancellationRequestedError)
|
||||||
{
|
{
|
||||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||||
@@ -388,7 +388,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
queryString = $"?{queryString}";
|
queryString = $"?{queryString}";
|
||||||
|
|
||||||
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||||
request.Accept = Constants.JsonContentHeader;
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
foreach (var header in requestConfiguration.Headers)
|
foreach (var header in requestConfiguration.Headers)
|
||||||
@@ -424,11 +424,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestDefinition">The request definition</param>
|
||||||
/// <param name="request">The request object to execute</param>
|
/// <param name="request">The request object to execute</param>
|
||||||
/// <param name="gate">The ratelimit gate used</param>
|
/// <param name="gate">The ratelimit gate used</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||||
|
RequestDefinition requestDefinition,
|
||||||
IRequest request,
|
IRequest request,
|
||||||
IRateLimitGate? gate,
|
IRateLimitGate? gate,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -441,14 +443,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
var statusCode = response.StatusCode;
|
|
||||||
var headers = response.ResponseHeaders;
|
|
||||||
var responseLength = response.ContentLength;
|
|
||||||
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||||
|
|
||||||
accessor = CreateAccessor();
|
accessor = CreateAccessor();
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||||
{
|
{
|
||||||
// Error response
|
// Error response
|
||||||
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||||
@@ -470,27 +469,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
|
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||||
if (typeof(T) == typeof(object))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||||
|
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
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, valid.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
|
// Json response received
|
||||||
var parsedError = TryParseError(response.ResponseHeaders, accessor);
|
var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
|
||||||
if (parsedError != null)
|
if (parsedError != null)
|
||||||
{
|
{
|
||||||
if (parsedError is ServerRateLimitError rateError)
|
if (parsedError is ServerRateLimitError rateError)
|
||||||
@@ -503,33 +500,55 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Success status code, but TryParseError determined it was an error response
|
// Success status code, but TryParseError determined it was an error response
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
var deserializeResult = accessor.Deserialize<T>();
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException requestException)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var error = new WebError(requestException.Message, requestException);
|
var error = new WebError(requestException.Message, requestException);
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
var error = new WebError($"Request timed out", exception: canceledException);
|
var error = new WebError($"Request timed out", exception: canceledException);
|
||||||
error.ErrorType = ErrorType.Timeout;
|
error.ErrorType = ErrorType.Timeout;
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (ArgumentException argumentException)
|
||||||
|
{
|
||||||
|
if (argumentException.Message.StartsWith("Only HTTP/"))
|
||||||
|
{
|
||||||
|
// Unsupported HTTP version error .net framework
|
||||||
|
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
|
||||||
|
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (NotSupportedException notSupportedException)
|
||||||
|
{
|
||||||
|
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
|
||||||
|
{
|
||||||
|
// Unsupported HTTP version error dotnet code
|
||||||
|
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
|
||||||
|
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
accessor?.Clear();
|
accessor?.Clear();
|
||||||
@@ -543,10 +562,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// This method will be called for each response to be able to check if the response is an error or not.
|
/// This method will be called for each response to be able to check if the response is an error or not.
|
||||||
/// If the response is an error this method should return the parsed error, else it should return null
|
/// If the response is an error this method should return the parsed error, else it should return null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestDefinition">Request definition</param>
|
||||||
/// <param name="accessor">Data accessor</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
/// <returns>Null if not an error, Error otherwise</returns>
|
||||||
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||||
@@ -673,21 +693,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
base.SetOptions(options);
|
base.SetOptions(options);
|
||||||
|
|
||||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
|
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
if (timeSyncParams == null)
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -716,7 +736,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldCache(RequestDefinition definition)
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
@@ -269,8 +269,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "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 waitEvent = new AsyncResetEvent(false);
|
||||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
|
||||||
if (subQuery != null)
|
if (subQuery != null)
|
||||||
{
|
{
|
||||||
// Send the request and wait for answer
|
// Send the request and wait for answer
|
||||||
@@ -279,7 +280,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
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
|
// No response received, but the subscription did receive updates. We'll assume success
|
||||||
}
|
}
|
||||||
@@ -287,6 +288,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
_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
|
// 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);
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -295,7 +297,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.Confirmed = true;
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
if (ct != default)
|
if (ct != default)
|
||||||
{
|
{
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||||
@@ -343,7 +345,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<THandlerResponse>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
@@ -494,25 +496,56 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, string? topic = null)
|
||||||
{
|
{
|
||||||
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
.Select(x => x.Value)
|
||||||
&& s.Value.Connected);
|
.ToList();
|
||||||
|
|
||||||
SocketConnection connection;
|
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||||
|
var delayStart = DateTime.UtcNow;
|
||||||
|
var delayed = false;
|
||||||
|
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
|
||||||
|
{
|
||||||
|
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
|
||||||
|
{
|
||||||
|
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
|
||||||
|
_logger.TimeoutWaitingForReconnectingSocket();
|
||||||
|
return new CallResult<SocketConnection>(new CantConnectError());
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
delayed = true;
|
||||||
|
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<SocketConnection>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayed)
|
||||||
|
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
|
||||||
|
|
||||||
|
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
|
||||||
|
&& (s.Authenticated == authenticated || !authenticated)
|
||||||
|
&& s.Connected).ToList();
|
||||||
|
|
||||||
|
SocketConnection? connection;
|
||||||
if (!dedicatedRequestConnection)
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||||
@@ -520,9 +553,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (connection != null)
|
if (connection != null)
|
||||||
{
|
{
|
||||||
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|
||||||
|
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||||
|
{
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(connection);
|
return new CallResult<SocketConnection>(connection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
@@ -716,7 +752,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
foreach (var item in DedicatedConnectionConfigs)
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.AsDataless();
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
@@ -813,7 +849,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
cs.SubscriptionStates.ForEach(subState =>
|
cs.SubscriptionStates.ForEach(subState =>
|
||||||
{
|
{
|
||||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||||
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
||||||
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
||||||
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
|
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvert(Type typeToConvert)
|
public override bool CanConvert(Type typeToConvert)
|
||||||
@@ -45,11 +45,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
if (reader.TokenType is JsonTokenType.Number)
|
if (reader.TokenType is JsonTokenType.Number)
|
||||||
{
|
{
|
||||||
var longValue = reader.GetDouble();
|
var decValue = reader.GetDecimal();
|
||||||
if (longValue == 0 || longValue < 0)
|
if (decValue == 0 || decValue < 0)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return ParseFromDouble(longValue);
|
return ParseFromDecimal(decValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|| stringValue == "-1"
|
|| stringValue == "-1"
|
||||||
|| stringValue == "0001-01-01T00:00:00Z"
|
|| stringValue == "0001-01-01T00:00:00Z"
|
||||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
@@ -88,20 +88,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a long value to datetime
|
/// Parse a double value to datetime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="longValue"></param>
|
public static DateTime ParseFromDouble(double value)
|
||||||
/// <returns></returns>
|
=> ParseFromDecimal((decimal)value);
|
||||||
public static DateTime ParseFromDouble(double longValue)
|
|
||||||
{
|
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
/// <summary>
|
||||||
|
/// Parse a decimal value to datetime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ParseFromDecimal(decimal value)
|
||||||
|
{
|
||||||
|
if (value < 19999999999)
|
||||||
|
return ConvertFromSeconds(value);
|
||||||
|
if (value < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(value);
|
||||||
|
if (value < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(value);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -152,19 +156,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||||
{
|
{
|
||||||
// Parse 1637745563.000 format
|
// Parse 1637745563.000 format
|
||||||
if (doubleValue <= 0)
|
if (decimalValue <= 0)
|
||||||
return default;
|
return default;
|
||||||
if (doubleValue < 19999999999)
|
if (decimalValue < 19999999999)
|
||||||
return ConvertFromSeconds(doubleValue);
|
return ConvertFromSeconds(decimalValue);
|
||||||
if (doubleValue < 19999999999999)
|
if (decimalValue < 19999999999999)
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
return ConvertFromMilliseconds(decimalValue);
|
||||||
if (doubleValue < 19999999999999999)
|
if (decimalValue < 19999999999999999)
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
return ConvertFromMicroseconds(decimalValue);
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
return ConvertFromNanoseconds(decimalValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
if (stringValue.Length == 10)
|
||||||
@@ -188,54 +192,70 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="seconds"></param>
|
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="milliseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="microseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nanoseconds"></param>
|
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
/// <returns></returns>
|
/// <summary>
|
||||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
[return: NotNullIfNotNull("time")]
|
||||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,10 +140,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringValue))
|
if (stringValue is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue!, out var result))
|
if (!GetValue(enumType, stringValue, out var result))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
{
|
{
|
||||||
@@ -204,6 +204,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
// An empty/null value will always fail when parsing, so just return here
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>9.5.0</PackageVersion>
|
<PackageVersion>9.10.0</PackageVersion>
|
||||||
<AssemblyVersion>9.5.0</AssemblyVersion>
|
<AssemblyVersion>9.10.0</AssemblyVersion>
|
||||||
<FileVersion>9.5.0</FileVersion>
|
<FileVersion>9.10.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ namespace CryptoExchange.Net
|
|||||||
value -= offset;
|
value -= offset;
|
||||||
else value += (step.Value - offset);
|
else value += (step.Value - offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
value = RoundDown(value, 8);
|
|
||||||
|
|
||||||
return value.Normalize();
|
return value.Normalize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is linear
|
/// Whether the trading mode is linear
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is inverse
|
/// Whether the trading mode is inverse
|
||||||
@@ -416,6 +416,36 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the account type is a futures account
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsFuturesAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.PerpetualLinearFutures
|
||||||
|
|| type == SharedAccountType.DeliveryLinearFutures
|
||||||
|
|| type == SharedAccountType.PerpetualInverseFutures
|
||||||
|
|| type == SharedAccountType.DeliveryInverseFutures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the account type is a margin account
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsMarginAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.CrossMargin
|
||||||
|
|| type == SharedAccountType.IsolatedMargin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map a TradingMode value to a SharedAccountType enum value
|
||||||
|
/// </summary>
|
||||||
|
public static SharedAccountType ToAccountType(this TradingMode mode)
|
||||||
|
{
|
||||||
|
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
|
||||||
|
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
|
||||||
|
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
|
||||||
|
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
|
||||||
|
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
|
||||||
|
|
||||||
|
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register rest client interfaces
|
/// Register rest client interfaces
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -445,6 +475,8 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||||
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
|
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Uri Uri { get; }
|
Uri Uri { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// HTTP protocol version
|
||||||
|
/// </summary>
|
||||||
|
Version HttpVersion { get; }
|
||||||
|
/// <summary>
|
||||||
/// internal request id for tracing
|
/// internal request id for tracing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int RequestId { get; }
|
int RequestId { get; }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -12,25 +13,21 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a request for an uri
|
/// Create a request for an uri
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="method"></param>
|
IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId);
|
||||||
/// <param name="uri"></param>
|
|
||||||
/// <param name="requestId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IRequest Create(HttpMethod method, Uri uri, int requestId);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configure the requests created by this factory
|
/// Configure the requests created by this factory
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestTimeout">Request timeout to use</param>
|
/// <param name="options">Rest client options</param>
|
||||||
/// <param name="httpClient">Optional shared http client instance</param>
|
/// <param name="httpClient">Optional shared http client instance</param>
|
||||||
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
void Configure(RestExchangeOptions options, HttpClient? httpClient = null);
|
||||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update settings
|
/// Update settings
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="proxy">Proxy to use</param>
|
/// <param name="proxy">Proxy to use</param>
|
||||||
/// <param name="requestTimeout">Request timeout to use</param>
|
/// <param name="requestTimeout">Request timeout to use</param>
|
||||||
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
|
/// <param name="httpKeepAliveInterval">Http client keep alive interval</param>
|
||||||
|
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -15,6 +16,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
HttpStatusCode StatusCode { get; }
|
HttpStatusCode StatusCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Http protocol version
|
||||||
|
/// </summary>
|
||||||
|
Version HttpVersion { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the status code indicates a success status
|
/// Whether the status code indicates a success status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Trackers.Klines;
|
||||||
|
using CryptoExchange.Net.Trackers.Trades;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tracker factory
|
||||||
|
/// </summary>
|
||||||
|
public interface ITrackerFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the factory supports creating a KlineTracker instance for this symbol and interval
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
/// <param name="interval">The kline interval</param>
|
||||||
|
bool CanCreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new kline tracker
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
/// <param name="interval">Kline interval</param>
|
||||||
|
/// <param name="limit">The max amount of klines to retain</param>
|
||||||
|
/// <param name="period">The max period the data should be retained</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the factory supports creating a TradeTracker instance for this symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
bool CanCreateTradeTracker(SharedSymbol symbol);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new trade tracker for a symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">The symbol</param>
|
||||||
|
/// <param name="limit">The max amount of trades to retain</param>
|
||||||
|
/// <param name="period">The max period the data should be retained</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
@@ -43,5 +46,58 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
return clientOrderId;
|
return clientOrderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new HttpMessageHandler instance
|
||||||
|
/// </summary>
|
||||||
|
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
|
||||||
|
{
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
var socketHandler = new SocketsHttpHandler();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
||||||
|
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
|
||||||
|
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException) { }
|
||||||
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
|
if (proxy != null)
|
||||||
|
{
|
||||||
|
socketHandler.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return socketHandler;
|
||||||
|
#else
|
||||||
|
var httpHandler = new HttpClientHandler();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
httpHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
|
httpHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException) { }
|
||||||
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
|
if (proxy != null)
|
||||||
|
{
|
||||||
|
httpHandler.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return httpHandler;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
||||||
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
||||||
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
|
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
|
||||||
|
private static readonly Action<ILogger, Exception?> _timeoutWaitingForReconnectingSocket;
|
||||||
|
private static readonly Action<ILogger, long, Exception?> _waitedForReconnectingSocket;
|
||||||
|
|
||||||
static SocketApiClientLoggingExtension()
|
static SocketApiClientLoggingExtension()
|
||||||
{
|
{
|
||||||
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(3018, "AddRetryAfterGuard"),
|
new EventId(3018, "AddRetryAfterGuard"),
|
||||||
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
||||||
|
|
||||||
|
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
|
||||||
|
"Timeout while waiting for existing socket reconnection, failing request");
|
||||||
|
|
||||||
|
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(3020, "WaitedForReconnectingSocket"),
|
||||||
|
"Waited for reconnecting socket for {Timespan}ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
||||||
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_addingRetryAfterGuard(logger, retryAfter, null);
|
_addingRetryAfterGuard(logger, retryAfter, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void TimeoutWaitingForReconnectingSocket(this ILogger logger)
|
||||||
|
{
|
||||||
|
_timeoutWaitingForReconnectingSocket(logger, null);
|
||||||
|
}
|
||||||
|
public static void WaitedForReconnectingSocket(this ILogger logger, long milliseconds)
|
||||||
|
{
|
||||||
|
_waitedForReconnectingSocket(logger, milliseconds, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// The request http method
|
/// The request http method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HttpMethod? RequestMethod { get; set; }
|
public HttpMethod? RequestMethod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP protocol version
|
||||||
|
/// </summary>
|
||||||
|
public Version? HttpVersion { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
@@ -251,6 +256,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
|
Version? httpVersion,
|
||||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
string? originalData,
|
string? originalData,
|
||||||
@@ -262,6 +268,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Error? error) : base(error)
|
Error? error) : base(error)
|
||||||
{
|
{
|
||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
|
HttpVersion = httpVersion;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
@@ -286,7 +293,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult AsError(Error error)
|
public WebCallResult AsError(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -297,7 +304,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult<K> As<K>([AllowNull] K data)
|
public WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -334,7 +341,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult<K> AsError<K>(Error error)
|
public WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -355,6 +362,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public HttpMethod? RequestMethod { get; set; }
|
public HttpMethod? RequestMethod { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP protocol version
|
||||||
|
/// </summary>
|
||||||
|
public Version? HttpVersion { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -403,21 +415,9 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new result
|
/// Create a new result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="responseHeaders"></param>
|
|
||||||
/// <param name="responseTime"></param>
|
|
||||||
/// <param name="responseLength"></param>
|
|
||||||
/// <param name="originalData"></param>
|
|
||||||
/// <param name="requestId"></param>
|
|
||||||
/// <param name="requestUrl"></param>
|
|
||||||
/// <param name="requestBody"></param>
|
|
||||||
/// <param name="requestMethod"></param>
|
|
||||||
/// <param name="requestHeaders"></param>
|
|
||||||
/// <param name="dataSource"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="error"></param>
|
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
|
Version? httpVersion,
|
||||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
@@ -431,6 +431,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
{
|
{
|
||||||
|
HttpVersion = httpVersion;
|
||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
@@ -450,7 +451,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDataless()
|
public new WebCallResult AsDataless()
|
||||||
{
|
{
|
||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy as a dataless result
|
/// Copy as a dataless result
|
||||||
@@ -458,14 +459,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDatalessError(Error error)
|
public new WebCallResult AsDatalessError(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <param name="error">The error</param>
|
||||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -475,7 +476,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -486,7 +487,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
public new WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -498,7 +499,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
|
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
|
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -569,7 +570,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
internal WebCallResult<T> Cached()
|
internal WebCallResult<T> Cached()
|
||||||
{
|
{
|
||||||
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -251,4 +251,47 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
DEX
|
DEX
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout behavior for queries
|
||||||
|
/// </summary>
|
||||||
|
public enum TimeoutBehavior
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fail the request
|
||||||
|
/// </summary>
|
||||||
|
Fail,
|
||||||
|
/// <summary>
|
||||||
|
/// Mark the query as successful
|
||||||
|
/// </summary>
|
||||||
|
Succeed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
private int? _code;
|
private int? _code;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The error code from the server
|
/// The int error code the server returned; or the http status code int value if there was no error code.<br />
|
||||||
|
/// <br />
|
||||||
|
/// <i>Note:</i><br />
|
||||||
|
/// The <see cref="ErrorCode"/> property should be used for more generic error checking; it might contain a string error code if the server does not return an int code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use ErrorCode instead", false)]
|
|
||||||
public int? Code
|
public int? Code
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
@@ -28,6 +30,20 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTTP protocol version to use, typically 2.0 or 1.1
|
||||||
|
/// </summary>
|
||||||
|
public Version HttpVersion { get; set; }
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
= new Version(2, 0);
|
||||||
|
#else
|
||||||
|
= new Version(1, 1);
|
||||||
|
#endif
|
||||||
|
/// <summary>
|
||||||
|
/// Http client keep alive interval for keeping connections open
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the values of this options on the target options
|
/// Set the values of this options on the target options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -43,6 +59,8 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||||
item.CachingEnabled = CachingEnabled;
|
item.CachingEnabled = CachingEnabled;
|
||||||
item.CachingMaxAge = CachingMaxAge;
|
item.CachingMaxAge = CachingMaxAge;
|
||||||
|
item.HttpVersion = HttpVersion;
|
||||||
|
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool PreventCaching { get; set; }
|
public bool PreventCaching { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the response to this requests should attempted to be parsed even when the status indicates failure
|
||||||
|
/// </summary>
|
||||||
|
public bool TryParseOnNonSuccess { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connection id
|
/// Connection id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="preventCaching">Prevent request caching</param>
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
|
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@@ -57,8 +58,9 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
bool? preventCaching = null)
|
bool? preventCaching = null,
|
||||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching);
|
bool? tryParseOnNonSuccess = null)
|
||||||
|
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||||
@@ -74,6 +76,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="preventCaching">Prevent request caching</param>
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
|
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
string identifier,
|
string identifier,
|
||||||
@@ -86,7 +89,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
bool? preventCaching = null)
|
bool? preventCaching = null,
|
||||||
|
bool? tryParseOnNonSuccess = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(identifier, out var def))
|
if (!_definitions.TryGetValue(identifier, out var def))
|
||||||
@@ -100,7 +104,8 @@ namespace CryptoExchange.Net.Objects
|
|||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
PreventCaching = preventCaching ?? false
|
PreventCaching = preventCaching ?? false,
|
||||||
|
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(identifier, def);
|
_definitions.TryAdd(identifier, def);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Sockets
|
namespace CryptoExchange.Net.Objects.Sockets
|
||||||
@@ -12,13 +14,27 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
private readonly SocketConnection _connection;
|
private readonly SocketConnection _connection;
|
||||||
private readonly Subscription _listener;
|
private readonly Subscription _listener;
|
||||||
|
|
||||||
|
private object _eventLock = new object();
|
||||||
|
private 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>
|
/// <summary>
|
||||||
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ConnectionLost
|
public event Action ConnectionLost
|
||||||
{
|
{
|
||||||
add => _connection.ConnectionLost += value;
|
add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
|
||||||
remove => _connection.ConnectionLost -= value;
|
remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -26,8 +42,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ConnectionClosed
|
public event Action ConnectionClosed
|
||||||
{
|
{
|
||||||
add => _connection.ConnectionClosed += value;
|
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
|
||||||
remove => _connection.ConnectionClosed -= value;
|
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -35,8 +51,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<Error> ResubscribingFailed
|
public event Action<Error> ResubscribingFailed
|
||||||
{
|
{
|
||||||
add => _connection.ResubscribingFailed += value;
|
add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
|
||||||
remove => _connection.ResubscribingFailed -= value;
|
remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -46,8 +62,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<TimeSpan> ConnectionRestored
|
public event Action<TimeSpan> ConnectionRestored
|
||||||
{
|
{
|
||||||
add => _connection.ConnectionRestored += value;
|
add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
|
||||||
remove => _connection.ConnectionRestored -= value;
|
remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,8 +71,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ActivityPaused
|
public event Action ActivityPaused
|
||||||
{
|
{
|
||||||
add => _connection.ActivityPaused += value;
|
add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
|
||||||
remove => _connection.ActivityPaused -= value;
|
remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,8 +80,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action ActivityUnpaused
|
public event Action ActivityUnpaused
|
||||||
{
|
{
|
||||||
add => _connection.ActivityUnpaused += value;
|
add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
|
||||||
remove => _connection.ActivityUnpaused -= value;
|
remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -95,7 +111,128 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
|
_connection.ConnectionClosed += HandleConnectionClosedEvent;
|
||||||
|
_connection.ConnectionLost += HandleConnectionLostEvent;
|
||||||
|
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
|
||||||
|
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
|
||||||
|
_connection.ActivityPaused += HandlePausedEvent;
|
||||||
|
_connection.ActivityUnpaused += HandleUnpausedEvent;
|
||||||
|
|
||||||
_listener = subscription;
|
_listener = subscription;
|
||||||
|
_listener.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>
|
/// <summary>
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Uri Uri => _request.RequestUri!;
|
public Uri Uri => _request.RequestUri!;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Version HttpVersion => _request.Version!;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int RequestId { get; }
|
public int RequestId { get; }
|
||||||
|
|
||||||
@@ -81,7 +84,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
|
public async Task<IResponse> GetResponseAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return new Response(await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false));
|
var response = await _httpClient.SendAsync(_request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new Response(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Requests
|
namespace CryptoExchange.Net.Requests
|
||||||
{
|
{
|
||||||
@@ -14,54 +15,43 @@ namespace CryptoExchange.Net.Requests
|
|||||||
private HttpClient? _httpClient;
|
private HttpClient? _httpClient;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
||||||
{
|
{
|
||||||
if (client == null)
|
if (client == null)
|
||||||
client = CreateClient(proxy, requestTimeout);
|
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
|
||||||
|
|
||||||
_httpClient = client;
|
_httpClient = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
|
||||||
{
|
{
|
||||||
if (_httpClient == null)
|
if (_httpClient == null)
|
||||||
throw new InvalidOperationException("Cant create request before configuring http client");
|
throw new InvalidOperationException("Cant create request before configuring http client");
|
||||||
|
|
||||||
return new Request(new HttpRequestMessage(method, uri), _httpClient, requestId);
|
var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
|
requestMessage.Version = httpRequestVersion;
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
|
||||||
|
#endif
|
||||||
|
return new Request(requestMessage, _httpClient, requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout)
|
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
_httpClient = CreateClient(proxy, requestTimeout);
|
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout)
|
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler();
|
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
|
||||||
try
|
|
||||||
{
|
|
||||||
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
|
||||||
handler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
|
||||||
}
|
|
||||||
catch (PlatformNotSupportedException) { }
|
|
||||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
|
||||||
|
|
||||||
if (proxy != null)
|
|
||||||
{
|
|
||||||
handler.Proxy = new WebProxy
|
|
||||||
{
|
|
||||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
|
||||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = new HttpClient(handler)
|
var client = new HttpClient(handler)
|
||||||
{
|
{
|
||||||
Timeout = requestTimeout
|
Timeout = requestTimeout
|
||||||
};
|
};
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -18,6 +19,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public HttpStatusCode StatusCode => _response.StatusCode;
|
public HttpStatusCode StatusCode => _response.StatusCode;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Version HttpVersion => _response.Version;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Account type
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedAccountType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unified account, combined account for multiple different types of trading
|
||||||
|
/// </summary>
|
||||||
|
Unified,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Funding account, where withdrawals and deposits are made from and to
|
||||||
|
/// </summary>
|
||||||
|
Funding,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spot trading account
|
||||||
|
/// </summary>
|
||||||
|
Spot,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cross margin account
|
||||||
|
/// </summary>
|
||||||
|
CrossMargin,
|
||||||
|
/// <summary>
|
||||||
|
/// Isolated margin account
|
||||||
|
/// </summary>
|
||||||
|
IsolatedMargin,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perpetual linear futures account
|
||||||
|
/// </summary>
|
||||||
|
PerpetualLinearFutures,
|
||||||
|
/// <summary>
|
||||||
|
/// Delivery linear futures account
|
||||||
|
/// </summary>
|
||||||
|
DeliveryLinearFutures,
|
||||||
|
/// <summary>
|
||||||
|
/// Perpetual inverse futures account
|
||||||
|
/// </summary>
|
||||||
|
PerpetualInverseFutures,
|
||||||
|
/// <summary>
|
||||||
|
/// Delivery inverse futures account
|
||||||
|
/// </summary>
|
||||||
|
DeliveryInverseFutures,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Option account
|
||||||
|
/// </summary>
|
||||||
|
Option,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Other
|
||||||
|
/// </summary>
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Balances request options
|
/// Balances request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
EndpointOptions<GetBalancesRequest> GetBalancesOptions { get; }
|
GetBalancesOptions GetBalancesOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get balances for the user
|
/// Get balances for the user
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for transferring funds between account types
|
||||||
|
/// </summary>
|
||||||
|
public interface ITransferRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Transfer request options
|
||||||
|
/// </summary>
|
||||||
|
TransferOptions TransferOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transfer funds between account types
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedId>> TransferAsync(TransferRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
INextPageToken? nextPageToken = null) :
|
INextPageToken? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
result.ResponseTime,
|
result.ResponseTime,
|
||||||
result.ResponseLength,
|
result.ResponseLength,
|
||||||
@@ -75,6 +76,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
INextPageToken? nextPageToken = null) :
|
INextPageToken? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
result.ResponseTime,
|
result.ResponseTime,
|
||||||
result.ResponseLength,
|
result.ResponseLength,
|
||||||
@@ -100,6 +102,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
string exchange,
|
string exchange,
|
||||||
TradingMode[]? dataTradeModes,
|
TradingMode[]? dataTradeModes,
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
|
Version? httpVersion,
|
||||||
KeyValuePair<string, string[]>[]? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
@@ -114,6 +117,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
Error? error,
|
Error? error,
|
||||||
INextPageToken? nextPageToken = null) : base(
|
INextPageToken? nextPageToken = null) : base(
|
||||||
code,
|
code,
|
||||||
|
httpVersion,
|
||||||
responseHeaders,
|
responseHeaders,
|
||||||
responseTime,
|
responseTime,
|
||||||
responseLength,
|
responseLength,
|
||||||
@@ -140,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for requesting a transfer
|
||||||
|
/// </summary>
|
||||||
|
public class GetBalancesOptions : EndpointOptions<GetBalancesRequest>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Supported account types
|
||||||
|
/// </summary>
|
||||||
|
public AccountTypeFilter[] SupportedAccountTypes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public GetBalancesOptions(params AccountTypeFilter[] accountTypes) : base(true)
|
||||||
|
{
|
||||||
|
SupportedAccountTypes = accountTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
public Error? ValidateRequest(
|
||||||
|
string exchange,
|
||||||
|
GetBalancesRequest request,
|
||||||
|
TradingMode[] supportedApiTypes)
|
||||||
|
{
|
||||||
|
if (request.AccountType != null && !IsValid(request.AccountType.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(request.AccountType), "Invalid AccountType");
|
||||||
|
|
||||||
|
return base.ValidateRequest(exchange, request, null, supportedApiTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the account type valid for this client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool IsValid(SharedAccountType accountType)
|
||||||
|
{
|
||||||
|
if (accountType == SharedAccountType.Funding)
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Funding);
|
||||||
|
|
||||||
|
if (accountType == SharedAccountType.Spot)
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Spot);
|
||||||
|
|
||||||
|
if (accountType == SharedAccountType.PerpetualLinearFutures
|
||||||
|
|| accountType == SharedAccountType.PerpetualInverseFutures
|
||||||
|
|| accountType == SharedAccountType.DeliveryLinearFutures
|
||||||
|
|| accountType == SharedAccountType.DeliveryInverseFutures)
|
||||||
|
{
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Futures);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (accountType == SharedAccountType.CrossMargin
|
||||||
|
|| accountType == SharedAccountType.IsolatedMargin)
|
||||||
|
{
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedAccountTypes.Contains(AccountTypeFilter.Option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account type filter
|
||||||
|
/// </summary>
|
||||||
|
public enum AccountTypeFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Funding account
|
||||||
|
/// </summary>
|
||||||
|
Funding,
|
||||||
|
/// <summary>
|
||||||
|
/// Spot account
|
||||||
|
/// </summary>
|
||||||
|
Spot,
|
||||||
|
/// <summary>
|
||||||
|
/// Futures account
|
||||||
|
/// </summary>
|
||||||
|
Futures,
|
||||||
|
/// <summary>
|
||||||
|
/// Margin account
|
||||||
|
/// </summary>
|
||||||
|
Margin,
|
||||||
|
/// <summary>
|
||||||
|
/// Option account
|
||||||
|
/// </summary>
|
||||||
|
Option
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for requesting a transfer
|
||||||
|
/// </summary>
|
||||||
|
public class TransferOptions : EndpointOptions<TransferRequest>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Supported account types
|
||||||
|
/// </summary>
|
||||||
|
public SharedAccountType[] SupportedAccountTypes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public TransferOptions(SharedAccountType[] accountTypes) : base(true)
|
||||||
|
{
|
||||||
|
SupportedAccountTypes = accountTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
public new Error? ValidateRequest(
|
||||||
|
string exchange,
|
||||||
|
TransferRequest request,
|
||||||
|
TradingMode? tradingMode,
|
||||||
|
TradingMode[] supportedApiTypes)
|
||||||
|
{
|
||||||
|
if (!SupportedAccountTypes.Contains(request.FromAccountType))
|
||||||
|
return ArgumentError.Invalid(nameof(request.FromAccountType), "Invalid FromAccountType");
|
||||||
|
|
||||||
|
if (!SupportedAccountTypes.Contains(request.ToAccountType))
|
||||||
|
return ArgumentError.Invalid(nameof(request.FromAccountType), "Invalid ToAccountType");
|
||||||
|
|
||||||
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,18 +8,28 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public record GetBalancesRequest : SharedRequest
|
public record GetBalancesRequest : SharedRequest
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trading mode
|
/// Account type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TradingMode? TradingMode { get; set; }
|
public SharedAccountType? AccountType { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tradingMode">Trading mode</param>
|
/// <param name="tradingMode">Trading mode</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetBalancesRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetBalancesRequest(TradingMode tradingMode, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
TradingMode = tradingMode;
|
AccountType = tradingMode.ToAccountType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountType">Account type</param>
|
||||||
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
|
public GetBalancesRequest(SharedAccountType? accountType = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
|
{
|
||||||
|
AccountType = accountType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request to transfer funds between account types
|
||||||
|
/// </summary>
|
||||||
|
public record TransferRequest : SharedRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Asset
|
||||||
|
/// </summary>
|
||||||
|
public string Asset { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Quantity
|
||||||
|
/// </summary>
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// From symbol
|
||||||
|
/// </summary>
|
||||||
|
public string? FromSymbol { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// To symbol
|
||||||
|
/// </summary>
|
||||||
|
public string? ToSymbol { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// From account type
|
||||||
|
/// </summary>
|
||||||
|
public SharedAccountType FromAccountType { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// To account type
|
||||||
|
/// </summary>
|
||||||
|
public SharedAccountType ToAccountType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="asset">The asset to transfer</param>
|
||||||
|
/// <param name="quantity">Quantity to transfer</param>
|
||||||
|
/// <param name="fromAccount">From account type</param>
|
||||||
|
/// <param name="toAccount">To account type</param>
|
||||||
|
/// <param name="fromSymbol">From symbol</param>
|
||||||
|
/// <param name="toSymbol">To symbol</param>
|
||||||
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
|
public TransferRequest(
|
||||||
|
string asset,
|
||||||
|
decimal quantity,
|
||||||
|
SharedAccountType fromAccount,
|
||||||
|
SharedAccountType toAccount,
|
||||||
|
string? fromSymbol = null,
|
||||||
|
string? toSymbol = null,
|
||||||
|
ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
|
{
|
||||||
|
Asset = asset;
|
||||||
|
Quantity = quantity;
|
||||||
|
FromAccountType = fromAccount;
|
||||||
|
ToAccountType = toAccount;
|
||||||
|
FromSymbol = fromSymbol;
|
||||||
|
ToSymbol = toSymbol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,6 +67,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Min number of confirmations
|
/// Min number of confirmations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MinConfirmations { get; set; }
|
public int? MinConfirmations { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The contract address
|
||||||
|
/// </summary>
|
||||||
|
public string? ContractAddress { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
/// Total quantity
|
/// Total quantity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal Total { get; set; }
|
public decimal Total { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Isolated margin symbol, only applicable for isolated margin futures
|
/// Isolated margin symbol, only applicable for isolated margin futures
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string OrderId { get; set; }
|
public string OrderId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// The client order id
|
||||||
|
/// </summary>
|
||||||
|
public string? ClientOrderId { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Side of the trade
|
/// Side of the trade
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SharedOrderSide? Side { get; set; }
|
public SharedOrderSide? Side { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.RateLimiting;
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -252,6 +253,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
return new CallResult(new ServerRateLimitError(we.Message, we));
|
return new CallResult(new ServerRateLimitError(we.Message, we));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_socket.HttpStatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
return new CallResult(new ServerError(new ErrorInfo(ErrorType.Unauthorized, "Server returned status code `401` when `101` was expected")));
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
|
// ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0
|
||||||
// Try to read 429 from the message instead
|
// Try to read 429 from the message instead
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? RequestTimeout { get; set; }
|
public TimeSpan? RequestTimeout { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What should happen if the query times out
|
||||||
|
/// </summary>
|
||||||
|
public TimeoutBehavior TimeoutBehavior { get; set; } = TimeoutBehavior.Fail;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
|
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
|
||||||
/// and each symbol receives it's own confirmation response
|
/// and each symbol receives it's own confirmation response
|
||||||
@@ -183,7 +188,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check)
|
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message, MessageHandlerLink check)
|
||||||
{
|
{
|
||||||
if (!PreCheckMessage(message))
|
if (!PreCheckMessage(connection, message))
|
||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
CurrentResponses++;
|
CurrentResponses++;
|
||||||
@@ -208,18 +213,20 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validate if a message is actually processable by this query
|
/// Validate if a message is actually processable by this query
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public virtual bool PreCheckMessage(SocketConnection connection, DataEvent<object> message) => true;
|
||||||
/// <returns></returns>
|
|
||||||
public virtual bool PreCheckMessage(DataEvent<object> message) => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Timeout()
|
public override void Timeout()
|
||||||
{
|
{
|
||||||
if (Completed)
|
if (Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Completed = true;
|
Completed = true;
|
||||||
Result = new CallResult<THandlerResponse>(new TimeoutError());
|
if (TimeoutBehavior == TimeoutBehavior.Fail)
|
||||||
|
Result = new CallResult<THandlerResponse>(new TimeoutError());
|
||||||
|
else
|
||||||
|
Result = new CallResult<THandlerResponse>(default, null, default);
|
||||||
|
|
||||||
ContinueAwaiter?.Set();
|
ContinueAwaiter?.Set();
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,18 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of current pending requests
|
||||||
|
/// </summary>
|
||||||
|
public int PendingRequests
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_listenersLock)
|
||||||
|
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
private readonly object _listenersLock;
|
private readonly object _listenersLock;
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
@@ -284,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
lock (_listenersLock)
|
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();
|
subscription.Reset();
|
||||||
|
|
||||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||||
@@ -515,11 +527,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
|
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
|
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||||
subscriptionProcessor.Confirmed = true;
|
subscriptionProcessor.Status = SubscriptionStatus.Subscribed;
|
||||||
// This doesn't trigger a waiting subscribe query, should probably also somehow set the wait event for that
|
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
|
// 5. Deserialize the message
|
||||||
@@ -642,13 +657,16 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public async Task CloseAsync(Subscription subscription)
|
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
|
// 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);
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
|
||||||
subscription.Closed = true;
|
subscription.Status = SubscriptionStatus.Closing;
|
||||||
|
|
||||||
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||||
|
{
|
||||||
|
subscription.Status = SubscriptionStatus.Closed;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.ClosingSubscription(SocketId, subscription.Id);
|
_logger.ClosingSubscription(SocketId, subscription.Id);
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
@@ -660,7 +678,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
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)
|
if (!anyDuplicateSubscription)
|
||||||
{
|
{
|
||||||
@@ -678,6 +696,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
if (Status == SocketStatus.Closing)
|
if (Status == SocketStatus.Closing)
|
||||||
{
|
{
|
||||||
|
subscription.Status = SubscriptionStatus.Closed;
|
||||||
_logger.AlreadyClosing(SocketId);
|
_logger.AlreadyClosing(SocketId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -685,12 +704,15 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (shouldCloseConnection)
|
if (shouldCloseConnection)
|
||||||
{
|
{
|
||||||
Status = SocketStatus.Closing;
|
Status = SocketStatus.Closing;
|
||||||
|
subscription.IsClosingConnection = true;
|
||||||
_logger.ClosingNoMoreSubscriptions(SocketId);
|
_logger.ClosingNoMoreSubscriptions(SocketId);
|
||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
_listeners.Remove(subscription);
|
_listeners.Remove(subscription);
|
||||||
|
|
||||||
|
subscription.Status = SubscriptionStatus.Closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -974,7 +996,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
List<Subscription> subList;
|
List<Subscription> subList;
|
||||||
lock (_listenersLock)
|
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)
|
if (subList.Count == 0)
|
||||||
break;
|
break;
|
||||||
@@ -983,34 +1005,32 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
foreach (var subscription in subList)
|
foreach (var subscription in subList)
|
||||||
{
|
{
|
||||||
subscription.ConnectionInvocations = 0;
|
subscription.ConnectionInvocations = 0;
|
||||||
if (subscription.Closed)
|
if (!subscription.Active)
|
||||||
// Can be closed during resubscribing
|
// Can be closed during resubscribing
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
subscription.IsResubscribing = true;
|
subscription.Status = SubscriptionStatus.Subscribing;
|
||||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||||
subscription.IsResubscribing = false;
|
subscription.Status = SubscriptionStatus.Pending;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var subQuery = subscription.GetSubQuery(this);
|
var subQuery = subscription.CreateSubscriptionQuery(this);
|
||||||
if (subQuery == null)
|
if (subQuery == null)
|
||||||
{
|
{
|
||||||
subscription.IsResubscribing = false;
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitEvent = new AsyncResetEvent(false);
|
var waitEvent = new AsyncResetEvent(false);
|
||||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||||
{
|
{
|
||||||
subscription.IsResubscribing = false;
|
subscription.Status = r.Result.Success ? SubscriptionStatus.Subscribed: SubscriptionStatus.Pending;
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
waitEvent.Set();
|
waitEvent.Set();
|
||||||
if (r.Result.Success)
|
|
||||||
subscription.Confirmed = true;
|
|
||||||
return r.Result;
|
return r.Result;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1031,7 +1051,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
internal async Task UnsubscribeAsync(Subscription subscription)
|
internal async Task UnsubscribeAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
var unsubscribeRequest = subscription.GetUnsubQuery();
|
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
|
||||||
if (unsubscribeRequest == null)
|
if (unsubscribeRequest == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1044,7 +1064,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket is not connected"));
|
return new CallResult(new WebError("Socket is not connected"));
|
||||||
|
|
||||||
var subQuery = subscription.GetSubQuery(this);
|
var subQuery = subscription.CreateSubscriptionQuery(this);
|
||||||
if (subQuery == null)
|
if (subQuery == null)
|
||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
|
|||||||
@@ -34,21 +34,33 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Is it a user subscription
|
/// Is it a user subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UserSubscription { get; set; }
|
public bool UserSubscription { get; set; }
|
||||||
|
|
||||||
|
private SubscriptionStatus _status;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Has the subscription been confirmed
|
/// Current subscription status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Confirmed { get; set; }
|
public SubscriptionStatus Status
|
||||||
|
{
|
||||||
|
get => _status;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_status == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_status = value;
|
||||||
|
Task.Run(() => StatusChanged?.Invoke(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the subscription closed
|
/// Whether the subscription is active
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Closed { get; set; }
|
public bool Active => Status != SubscriptionStatus.Closing && Status != SubscriptionStatus.Closed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the subscription currently resubscribing
|
/// Whether the unsubscribing of this subscription lead to the closing of the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsResubscribing { get; set; }
|
public bool IsClosingConnection { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logger
|
/// Logger
|
||||||
@@ -74,12 +86,26 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Exception event
|
/// Exception event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<Exception>? Exception;
|
public event Action<Exception>? Exception;
|
||||||
|
/// <summary>
|
||||||
|
/// Listener unsubscribed event
|
||||||
|
/// </summary>
|
||||||
|
public event Action<SubscriptionStatus>? StatusChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subscription topic
|
/// Subscription topic
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Topic { get; set; }
|
public string? Topic { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The subscribe query for this subscription
|
||||||
|
/// </summary>
|
||||||
|
public Query? SubscriptionQuery { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The unsubscribe query for this subscription
|
||||||
|
/// </summary>
|
||||||
|
public Query? UnsubscriptionQuery { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -91,11 +117,21 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
Id = ExchangeHelpers.NextId();
|
Id = ExchangeHelpers.NextId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new subscription query
|
||||||
|
/// </summary>
|
||||||
|
public Query? CreateSubscriptionQuery(SocketConnection connection)
|
||||||
|
{
|
||||||
|
var query = GetSubQuery(connection);
|
||||||
|
SubscriptionQuery = query;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the subscribe query to send when subscribing
|
/// Get the subscribe query to send when subscribing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public abstract Query? GetSubQuery(SocketConnection connection);
|
protected abstract Query? GetSubQuery(SocketConnection connection);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a subscription query response
|
/// Handle a subscription query response
|
||||||
@@ -109,11 +145,21 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public virtual void HandleUnsubQueryResponse(object message) { }
|
public virtual void HandleUnsubQueryResponse(object message) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new unsubscription query
|
||||||
|
/// </summary>
|
||||||
|
public Query? CreateUnsubscriptionQuery(SocketConnection connection)
|
||||||
|
{
|
||||||
|
var query = GetUnsubQuery(connection);
|
||||||
|
UnsubscriptionQuery = query;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the unsubscribe query to send when unsubscribing
|
/// Get the unsubscribe query to send when unsubscribing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public abstract Query? GetUnsubQuery();
|
protected abstract Query? GetUnsubQuery(SocketConnection connection);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
||||||
@@ -133,7 +179,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Confirmed = false;
|
Status = SubscriptionStatus.Pending;
|
||||||
DoHandleReset();
|
DoHandleReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +201,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// State of this subscription
|
/// State of this subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Id">The id of the subscription</param>
|
/// <param name="Id">The id of the subscription</param>
|
||||||
/// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param>
|
/// <param name="Status">Subscription status</param>
|
||||||
/// <param name="Invocations">Number of times this subscription got a message</param>
|
/// <param name="Invocations">Number of times this subscription got a message</param>
|
||||||
/// <param name="ListenMatcher">Matcher for this subscription</param>
|
/// <param name="ListenMatcher">Matcher for this subscription</param>
|
||||||
public record SubscriptionState(
|
public record SubscriptionState(
|
||||||
int Id,
|
int Id,
|
||||||
bool Confirmed,
|
SubscriptionStatus Status,
|
||||||
int Invocations,
|
int Invocations,
|
||||||
MessageMatcher ListenMatcher
|
MessageMatcher ListenMatcher
|
||||||
);
|
);
|
||||||
@@ -171,7 +217,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public SubscriptionState GetState()
|
public SubscriptionState GetState()
|
||||||
{
|
{
|
||||||
return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher);
|
return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="authenticated"></param>
|
/// <param name="authenticated"></param>
|
||||||
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
|
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
|
||||||
{
|
{
|
||||||
Confirmed = true;
|
Status = SubscriptionStatus.Subscribed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Query? GetSubQuery(SocketConnection connection) => null;
|
protected override Query? GetSubQuery(SocketConnection connection) => null;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Query? GetUnsubQuery() => null;
|
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
|||||||
var value = jsonValue.GetDecimal();
|
var value = jsonValue.GetDecimal();
|
||||||
if (objectValue is DateTime time)
|
if (objectValue is DateTime time)
|
||||||
{
|
{
|
||||||
if (time != DateTimeConverter.ParseFromDouble((double)value))
|
if (time != DateTimeConverter.ParseFromDecimal(value))
|
||||||
throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}");
|
throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}");
|
||||||
}
|
}
|
||||||
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
|
|
||||||
public Uri Uri { get; set; }
|
public Uri Uri { get; set; }
|
||||||
|
|
||||||
|
public Version HttpVersion { get; set; }
|
||||||
|
|
||||||
public int RequestId { get; set; }
|
public int RequestId { get; set; }
|
||||||
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -14,11 +15,11 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
_request = request;
|
_request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null)
|
public void Configure(RestExchangeOptions options, HttpClient? client)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
public IRequest Create(Version httpRequestVersion, HttpMethod method, Uri uri, int requestId)
|
||||||
{
|
{
|
||||||
_request.Method = method;
|
_request.Method = method;
|
||||||
_request.Uri = uri;
|
_request.Uri = uri;
|
||||||
@@ -26,6 +27,6 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
return _request;
|
return _request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout) {}
|
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
private readonly Stream _response;
|
private readonly Stream _response;
|
||||||
|
|
||||||
public HttpStatusCode StatusCode { get; }
|
public HttpStatusCode StatusCode { get; }
|
||||||
|
public Version HttpVersion { get; }
|
||||||
|
|
||||||
public bool IsSuccessStatusCode { get; }
|
public bool IsSuccessStatusCode { get; }
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
|||||||
public TestResponse(HttpStatusCode code, Stream response)
|
public TestResponse(HttpStatusCode code, Stream response)
|
||||||
{
|
{
|
||||||
StatusCode = code;
|
StatusCode = code;
|
||||||
|
HttpVersion = new Version(2, 0);
|
||||||
IsSuccessStatusCode = code == HttpStatusCode.OK;
|
IsSuccessStatusCode = code == HttpStatusCode.OK;
|
||||||
_response = response;
|
_response = response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,7 @@ namespace CryptoExchange.Net.Testing
|
|||||||
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null;
|
var uriParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InUri ? client.CreateParameterDictionary(parameters) : null;
|
||||||
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null;
|
var bodyParams = client.ParameterPositions[method] == HttpMethodParameterPosition.InBody ? client.CreateParameterDictionary(parameters) : null;
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var requestDefinition = new RestRequestConfiguration(
|
||||||
|
|
||||||
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
|
|
||||||
authProvider.ProcessRequest(
|
|
||||||
client,
|
|
||||||
new RestRequestConfiguration(
|
|
||||||
new RequestDefinition(path, method)
|
new RequestDefinition(path, method)
|
||||||
{
|
{
|
||||||
Authenticated = true
|
Authenticated = true
|
||||||
@@ -141,14 +136,19 @@ namespace CryptoExchange.Net.Testing
|
|||||||
host,
|
host,
|
||||||
uriParams ?? new Dictionary<string, object>(),
|
uriParams ?? new Dictionary<string, object>(),
|
||||||
bodyParams ?? new Dictionary<string, object>(),
|
bodyParams ?? new Dictionary<string, object>(),
|
||||||
headers,
|
new Dictionary<string, string>(),
|
||||||
client.ArraySerialization,
|
client.ArraySerialization,
|
||||||
client.ParameterPositions[method],
|
client.ParameterPositions[method],
|
||||||
client.RequestBodyFormat
|
client.RequestBodyFormat
|
||||||
)
|
);
|
||||||
|
|
||||||
|
authProvider.TimeProvider = new TestAuthTimeProvider(time ?? new DateTime(2024, 01, 01, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
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))
|
if (!string.Equals(signature, expectedSignature, compareCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
|
||||||
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
|
throw new Exception($"Signatures do not match. Expected: {expectedSignature}, Actual: {signature}");
|
||||||
|
|||||||
@@ -5,29 +5,31 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="11.3.0" />
|
<PackageReference Include="Binance.Net" Version="11.8.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="9.3.0" />
|
<PackageReference Include="Bitfinex.Net" Version="9.8.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="2.4.1" />
|
<PackageReference Include="BitMart.Net" Version="2.9.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="5.4.0" />
|
<PackageReference Include="BloFin.Net" Version="1.1.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="9.3.0" />
|
<PackageReference Include="Bybit.Net" Version="5.9.0" />
|
||||||
<PackageReference Include="CoinW.Net" Version="1.0.1" />
|
<PackageReference Include="CoinEx.Net" Version="9.8.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="2.4.0" />
|
<PackageReference Include="CoinW.Net" Version="1.5.0" />
|
||||||
<PackageReference Include="DeepCoin.Net" Version="2.3.0" />
|
<PackageReference Include="CryptoCom.Net" Version="2.9.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="2.4.0" />
|
<PackageReference Include="DeepCoin.Net" Version="2.8.0" />
|
||||||
<PackageReference Include="HyperLiquid.Net" Version="2.4.0" />
|
<PackageReference Include="GateIo.Net" Version="2.9.1" />
|
||||||
<PackageReference Include="JK.BingX.Net" Version="2.3.0" />
|
<PackageReference Include="HyperLiquid.Net" Version="2.13.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="2.3.0" />
|
<PackageReference Include="JK.BingX.Net" Version="2.8.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="3.3.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="2.8.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="3.3.1" />
|
<PackageReference Include="JK.Mexc.Net" Version="3.9.0" />
|
||||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.3.0" />
|
<PackageReference Include="JK.OKX.Net" Version="3.8.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.3.0" />
|
<PackageReference Include="Jkorf.Aster.Net" Version="1.0.0" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="7.3.0" />
|
<PackageReference Include="JKorf.BitMEX.Net" Version="2.8.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="6.3.1" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="2.8.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="7.3.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="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
<PackageReference Include="Toobit.Net" Version="1.2.1" />
|
<PackageReference Include="Toobit.Net" Version="1.7.0" />
|
||||||
<PackageReference Include="WhiteBit.Net" Version="2.4.0" />
|
<PackageReference Include="WhiteBit.Net" Version="2.9.0" />
|
||||||
<PackageReference Include="XT.Net" Version="2.3.1" />
|
<PackageReference Include="XT.Net" Version="2.8.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@inject IAsterRestClient asterClient
|
||||||
@inject IBinanceRestClient binanceClient
|
@inject IBinanceRestClient binanceClient
|
||||||
@inject IBingXRestClient bingXClient
|
@inject IBingXRestClient bingXClient
|
||||||
@inject IBitfinexRestClient bitfinexClient
|
@inject IBitfinexRestClient bitfinexClient
|
||||||
@inject IBitgetRestClient bitgetClient
|
@inject IBitgetRestClient bitgetClient
|
||||||
@inject IBitMartRestClient bitmartClient
|
@inject IBitMartRestClient bitmartClient
|
||||||
@inject IBitMEXRestClient bitmexClient
|
@inject IBitMEXRestClient bitmexClient
|
||||||
|
@inject IBloFinRestClient bloFinClient
|
||||||
@inject IBybitRestClient bybitClient
|
@inject IBybitRestClient bybitClient
|
||||||
@inject ICoinbaseRestClient coinbaseClient
|
@inject ICoinbaseRestClient coinbaseClient
|
||||||
@inject ICoinExRestClient coinexClient
|
@inject ICoinExRestClient coinexClient
|
||||||
@@ -33,12 +35,14 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
var asterTask = asterClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||||
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
|
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||||
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
|
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
|
||||||
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
|
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
|
||||||
|
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||||
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
|
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
|
||||||
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
|
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
|
||||||
@@ -47,7 +51,7 @@
|
|||||||
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
|
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
|
||||||
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||||
var htxTask = htxClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
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 krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||||
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
@@ -56,7 +60,10 @@
|
|||||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||||
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
|
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)
|
if (binanceTask.Result.Success)
|
||||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||||
@@ -76,6 +83,9 @@
|
|||||||
if (bitmexTask.Result.Success)
|
if (bitmexTask.Result.Success)
|
||||||
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
|
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
|
||||||
|
|
||||||
|
if (bloFinTask.Result.Success)
|
||||||
|
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
|
||||||
|
|
||||||
if (bybitTask.Result.Success)
|
if (bybitTask.Result.Success)
|
||||||
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
@page "/LiveData"
|
@page "/LiveData"
|
||||||
|
@inject IAsterSocketClient asterSocketClient
|
||||||
@inject IBinanceSocketClient binanceSocketClient
|
@inject IBinanceSocketClient binanceSocketClient
|
||||||
@inject IBingXSocketClient bingXSocketClient
|
@inject IBingXSocketClient bingXSocketClient
|
||||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||||
@inject IBitgetSocketClient bitgetSocketClient
|
@inject IBitgetSocketClient bitgetSocketClient
|
||||||
@inject IBitMartSocketClient bitmartSocketClient
|
@inject IBitMartSocketClient bitmartSocketClient
|
||||||
@inject IBitMEXSocketClient bitmexSocketClient
|
@inject IBitMEXSocketClient bitmexSocketClient
|
||||||
|
@inject IBloFinSocketClient bloFinSocketClient
|
||||||
@inject IBybitSocketClient bybitSocketClient
|
@inject IBybitSocketClient bybitSocketClient
|
||||||
@inject ICoinbaseSocketClient coinbaseSocketClient
|
@inject ICoinbaseSocketClient coinbaseSocketClient
|
||||||
@inject ICoinExSocketClient coinExSocketClient
|
@inject ICoinExSocketClient coinExSocketClient
|
||||||
@@ -42,12 +44,16 @@
|
|||||||
{
|
{
|
||||||
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
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)),
|
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||||
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
||||||
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
||||||
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
||||||
|
// BloFin doesn't support the ETH/BTC pair
|
||||||
|
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
|
||||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||||
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
|
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
|
||||||
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
|
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
|
||||||
@@ -61,7 +67,8 @@
|
|||||||
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
|
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
|
||||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
|
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
|
||||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||||
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
// Mexc doesn't offer a ticker stream currently
|
||||||
|
//mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
||||||
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
|
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
|
||||||
// Toobit doesn't support the ETH/BTC pair
|
// Toobit doesn't support the ETH/BTC pair
|
||||||
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
|
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
@page "/OrderBooks"
|
@page "/OrderBooks"
|
||||||
@using System.Collections.Concurrent
|
@using System.Collections.Concurrent
|
||||||
@using System.Timers
|
@using System.Timers
|
||||||
|
@using Aster.Net.Interfaces
|
||||||
@using Binance.Net.Interfaces
|
@using Binance.Net.Interfaces
|
||||||
@using BingX.Net.Interfaces
|
@using BingX.Net.Interfaces
|
||||||
@using Bitfinex.Net.Interfaces
|
@using Bitfinex.Net.Interfaces
|
||||||
@using Bitget.Net.Interfaces;
|
@using Bitget.Net.Interfaces;
|
||||||
@using BitMart.Net.Interfaces;
|
@using BitMart.Net.Interfaces;
|
||||||
@using BitMEX.Net.Interfaces;
|
@using BitMEX.Net.Interfaces;
|
||||||
|
@using BloFin.Net.Interfaces;
|
||||||
@using Bybit.Net.Interfaces
|
@using Bybit.Net.Interfaces
|
||||||
@using CoinEx.Net.Interfaces
|
@using CoinEx.Net.Interfaces
|
||||||
@using CoinW.Net.Interfaces
|
@using CoinW.Net.Interfaces
|
||||||
@@ -26,12 +28,14 @@
|
|||||||
@using Toobit.Net.Interfaces;
|
@using Toobit.Net.Interfaces;
|
||||||
@using WhiteBit.Net.Interfaces
|
@using WhiteBit.Net.Interfaces
|
||||||
@using XT.Net.Interfaces
|
@using XT.Net.Interfaces
|
||||||
|
@inject IAsterOrderBookFactory asterFactory
|
||||||
@inject IBinanceOrderBookFactory binanceFactory
|
@inject IBinanceOrderBookFactory binanceFactory
|
||||||
@inject IBingXOrderBookFactory bingXFactory
|
@inject IBingXOrderBookFactory bingXFactory
|
||||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||||
@inject IBitgetOrderBookFactory bitgetFactory
|
@inject IBitgetOrderBookFactory bitgetFactory
|
||||||
@inject IBitMartOrderBookFactory bitmartFactory
|
@inject IBitMartOrderBookFactory bitmartFactory
|
||||||
@inject IBitMEXOrderBookFactory bitmexFactory
|
@inject IBitMEXOrderBookFactory bitmexFactory
|
||||||
|
@inject IBloFinOrderBookFactory bloFinFactory
|
||||||
@inject IBybitOrderBookFactory bybitFactory
|
@inject IBybitOrderBookFactory bybitFactory
|
||||||
@inject ICoinbaseOrderBookFactory coinbaseFactory
|
@inject ICoinbaseOrderBookFactory coinbaseFactory
|
||||||
@inject ICoinExOrderBookFactory coinExFactory
|
@inject ICoinExOrderBookFactory coinExFactory
|
||||||
@@ -55,7 +59,7 @@
|
|||||||
@foreach(var book in _books.OrderBy(p => p.Key))
|
@foreach(var book in _books.OrderBy(p => p.Key))
|
||||||
{
|
{
|
||||||
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
|
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
|
||||||
<h4>@book.Key</h4>
|
<h4>@book.Key - @book.Value.Symbol</h4>
|
||||||
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
|
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 3; i++)
|
||||||
@@ -81,29 +85,28 @@
|
|||||||
|
|
||||||
_books = new Dictionary<string, ISymbolOrderBook>
|
_books = new Dictionary<string, ISymbolOrderBook>
|
||||||
{
|
{
|
||||||
|
{ "Aster", binanceFactory.CreateSpot("ETHUSDT") },
|
||||||
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||||
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
|
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
|
||||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||||
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||||
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
|
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
|
||||||
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
|
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
|
||||||
|
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
|
||||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||||
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
||||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||||
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
|
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
|
||||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||||
// DeepCoin does not support the ETH/BTC pair
|
{ "DeepCoin", deepCoinFactory.Create("ETH-USDT") },
|
||||||
//{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
|
|
||||||
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
||||||
// HyperLiquid does not support the ETH/BTC pair
|
{ "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
|
||||||
//{ "HyperLiquid", hyperLiquidFactory.Create("ETH/BTC") },
|
|
||||||
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
|
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
|
||||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||||
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
||||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||||
// Toobit does not support the ETH/BTC pair
|
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
|
||||||
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
|
|
||||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||||
{ "XT", xtFactory.CreateSpot("eth_btc") },
|
{ "XT", xtFactory.CreateSpot("eth_btc") },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
@page "/Trackers"
|
@page "/Trackers"
|
||||||
@using System.Collections.Concurrent
|
@using System.Collections.Concurrent
|
||||||
@using System.Timers
|
@using System.Timers
|
||||||
|
@using Aster.Net.Interfaces
|
||||||
@using Binance.Net.Interfaces
|
@using Binance.Net.Interfaces
|
||||||
@using BingX.Net.Interfaces
|
@using BingX.Net.Interfaces
|
||||||
@using Bitfinex.Net.Interfaces
|
@using Bitfinex.Net.Interfaces
|
||||||
@using Bitget.Net.Interfaces;
|
@using Bitget.Net.Interfaces;
|
||||||
@using BitMEX.Net.Interfaces;
|
@using BitMEX.Net.Interfaces;
|
||||||
@using BitMart.Net.Interfaces;
|
@using BitMart.Net.Interfaces;
|
||||||
|
@using BloFin.Net.Interfaces
|
||||||
@using Bybit.Net.Interfaces
|
@using Bybit.Net.Interfaces
|
||||||
@using CoinEx.Net.Interfaces
|
@using CoinEx.Net.Interfaces
|
||||||
@using CoinW.Net.Interfaces
|
@using CoinW.Net.Interfaces
|
||||||
@@ -27,12 +29,14 @@
|
|||||||
@using Toobit.Net.Interfaces;
|
@using Toobit.Net.Interfaces;
|
||||||
@using WhiteBit.Net.Interfaces
|
@using WhiteBit.Net.Interfaces
|
||||||
@using XT.Net.Interfaces
|
@using XT.Net.Interfaces
|
||||||
|
@inject IAsterTrackerFactory asterFactory
|
||||||
@inject IBinanceTrackerFactory binanceFactory
|
@inject IBinanceTrackerFactory binanceFactory
|
||||||
@inject IBingXTrackerFactory bingXFactory
|
@inject IBingXTrackerFactory bingXFactory
|
||||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||||
@inject IBitgetTrackerFactory bitgetFactory
|
@inject IBitgetTrackerFactory bitgetFactory
|
||||||
@inject IBitMartTrackerFactory bitmartFactory
|
@inject IBitMartTrackerFactory bitmartFactory
|
||||||
@inject IBitMEXTrackerFactory bitmexFactory
|
@inject IBitMEXTrackerFactory bitmexFactory
|
||||||
|
@inject IBloFinTrackerFactory bloFinFactory
|
||||||
@inject IBybitTrackerFactory bybitFactory
|
@inject IBybitTrackerFactory bybitFactory
|
||||||
@inject ICoinbaseTrackerFactory coinbaseFactory
|
@inject ICoinbaseTrackerFactory coinbaseFactory
|
||||||
@inject ICoinExTrackerFactory coinExFactory
|
@inject ICoinExTrackerFactory coinExFactory
|
||||||
@@ -71,33 +75,36 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var usdtSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
var usdcSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDC");
|
||||||
|
var usdtSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var usdtPerpLinSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||||
|
|
||||||
_trackers = new List<ITradeTracker>
|
_trackers = new List<ITradeTracker>
|
||||||
{
|
{
|
||||||
{ binanceFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ asterFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ bingXFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ bitfinexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ bitgetFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ bitmartFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bitgetFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ bitmexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bitmartFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bitmexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bloFinFactory.CreateTradeTracker(usdtPerpLinSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ bybitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ coinWFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ coinbaseFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ coinExFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ coinWFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ cryptocomFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ htxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ deepCoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
// HyperLiquid doesn't support spot pair, but does have a futures BTC/USDC pair
|
{ gateioFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ hyperLiquidFactory.CreateTradeTracker(new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDC"), period: TimeSpan.FromMinutes(5)) },
|
{ htxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ krakenFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ hyperLiquidFactory.CreateTradeTracker(usdcSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ krakenFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ kucoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ toobitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
{ whitebitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
|
{ xtFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||||
};
|
};
|
||||||
|
|
||||||
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
|
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ namespace BlazorClient
|
|||||||
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddAster();
|
||||||
services.AddBingX();
|
services.AddBingX();
|
||||||
services.AddBitfinex();
|
services.AddBitfinex();
|
||||||
services.AddBitget();
|
services.AddBitget();
|
||||||
services.AddBitMart();
|
services.AddBitMart();
|
||||||
services.AddBitMEX();
|
services.AddBitMEX();
|
||||||
|
services.AddBloFin();
|
||||||
services.AddBybit();
|
services.AddBybit();
|
||||||
services.AddCoinbase();
|
services.AddCoinbase();
|
||||||
services.AddCoinEx();
|
services.AddCoinEx();
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using BlazorClient
|
@using BlazorClient
|
||||||
@using BlazorClient.Shared
|
@using BlazorClient.Shared
|
||||||
|
@using Aster.Net.Interfaces.Clients;
|
||||||
@using Binance.Net.Interfaces.Clients;
|
@using Binance.Net.Interfaces.Clients;
|
||||||
@using BingX.Net.Interfaces.Clients;
|
@using BingX.Net.Interfaces.Clients;
|
||||||
@using Bitfinex.Net.Interfaces.Clients;
|
@using Bitfinex.Net.Interfaces.Clients;
|
||||||
@using Bitget.Net.Interfaces.Clients;
|
@using Bitget.Net.Interfaces.Clients;
|
||||||
@using BitMart.Net.Interfaces.Clients;
|
@using BitMart.Net.Interfaces.Clients;
|
||||||
@using BitMEX.Net.Interfaces.Clients;
|
@using BitMEX.Net.Interfaces.Clients;
|
||||||
|
@using BloFin.Net.Interfaces.Clients;
|
||||||
@using Bybit.Net.Interfaces.Clients;
|
@using Bybit.Net.Interfaces.Clients;
|
||||||
@using Coinbase.Net.Interfaces.Clients;
|
@using Coinbase.Net.Interfaces.Clients;
|
||||||
@using CoinEx.Net.Interfaces.Clients;
|
@using CoinEx.Net.Interfaces.Clients;
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
|||||||
|
|
||||||
||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|
||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%|
|
||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%|
|
||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)|-|-|
|
||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%|
|
||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%|
|
||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%|
|
||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)|-|
|
||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)|-|
|
||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%|
|
||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%|
|
||||||
@@ -38,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.
|
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
|
||||||
|
|
||||||
|
### Full demo application
|
||||||
|
A full demo application is available using the [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) library:
|
||||||
|
https://github.com/JKorf/CryptoManager.Net
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
[](https://discord.gg/MSpeEtSY8t)
|
[](https://discord.gg/MSpeEtSY8t)
|
||||||
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
||||||
@@ -59,6 +65,37 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
|||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 9.10.0 - 15 Oct 2025
|
||||||
|
* Added ITransferRestClient Shared interface
|
||||||
|
* Updated IBalanceRestClient, GetBalancesRequest now mainly works with SharedAccountType type, allowing more options
|
||||||
|
* Updated IBalanceRestClient, GetBalanceOptions now specifies supported account types
|
||||||
|
|
||||||
|
* 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
|
* Version 9.5.0 - 19 Aug 2025
|
||||||
* Added better error handling support
|
* Added better error handling support
|
||||||
* Added ErrorDescription, ErrorType and IsTransient to Error object
|
* Added ErrorDescription, ErrorType and IsTransient to Error object
|
||||||
|
|||||||
Reference in New Issue
Block a user