mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c62775813f | |||
| f11b3754f0 | |||
| 3cbe0465e9 | |||
| 5048aea722 | |||
| c3316a51e7 | |||
| 7ecf37064b | |||
| 18954f4f53 | |||
| 00bc245102 | |||
| 273cab9fdb | |||
| 84d0f0ec9e | |||
| 690f2a63e5 | |||
| 19cc020852 | |||
| 1d4353e6d1 | |||
| a02b3f88d7 | |||
| af44ca4c9f | |||
| cf2b57bb96 | |||
| d0a2288910 | |||
| 89c11afc21 | |||
| da6ed580f1 | |||
| 1c33e297e7 | |||
| 0005534a95 | |||
| 11650f7c1a | |||
| a222bb3f02 | |||
| 6361c5ef25 | |||
| 892e8a4508 | |||
| 8336d373f3 | |||
| 71072680a8 | |||
| e13f105019 | |||
| 401577451e | |||
| 5c41ef1ee4 | |||
| ad614830d1 | |||
| 3365837338 | |||
| 66ac2972d6 | |||
| 0d3e05880a | |||
| 997e71f3b7 | |||
| b0fca4587d | |||
| c10671768d | |||
| 91e8123679 | |||
| 417cf2f9ac | |||
| 277be7ab9b | |||
| 45f3459f59 | |||
| 98dad4a8ed | |||
| 1e5f19271b | |||
| 8abeeb4cf0 | |||
| cae0cd9ead | |||
| 811574ae01 | |||
| 0ddecf7f8d | |||
| 5bcf50fb4d | |||
| 9f0654815d | |||
| 465e9f04f4 | |||
| 7c8cbfa4e2 | |||
| 4c79d13ff9 | |||
| c815fad135 | |||
| 41f17d0378 | |||
| 50715ff2f7 | |||
| ea9375d582 | |||
| 2cf3c93e5e | |||
| ca888d8e41 | |||
| 2040b1c175 | |||
| d451c18821 | |||
| c13dfa4461 | |||
| c2080ef75f | |||
| 6b252e8024 | |||
| d06bd5f176 | |||
| d55fc8da65 | |||
| 01184f2c5d | |||
| cadc93c2f0 | |||
| 2600a51461 | |||
| 9e6a86ba8b | |||
| c4430d63fa | |||
| f3e1cfef33 | |||
| cc3053719c | |||
| cd6907e601 | |||
| 8fe00693bd | |||
| fb90d1e015 | |||
| 4b44861e43 | |||
| e42ca4ab5a | |||
| 5b97f6dd67 | |||
| a9813ecb0a | |||
| c7069a4049 | |||
| 5683ae0b3c | |||
| 1c8cf5ac98 | |||
| ad7231ec56 | |||
| 7e4a607391 | |||
| 2d470d18e2 | |||
| cb9a766c3b | |||
| 94b8184f7b | |||
| 270ea06f24 | |||
| 536afa92da | |||
| 11c48b3341 | |||
| f514e172d7 | |||
| 1739769f87 | |||
| 7ccf643a34 | |||
| edfaa650bf | |||
| 13c81afb79 | |||
| c4f4ddcdc5 | |||
| 4f4d2ccff3 | |||
| 4db43517b7 | |||
| 41f38e040e | |||
| 3bdb50b1df | |||
| 0d1ca30ce3 | |||
| d5697250e2 | |||
| a54a327f22 | |||
| 7166482a46 | |||
| 839f509fef | |||
| 949b205d4f | |||
| e6c3251067 | |||
| 77611a19c8 | |||
| 9b950cab4c | |||
| 8b9172ba94 | |||
| d8a1d96e5c | |||
| 7b370d47ce | |||
| 434d9e3af6 | |||
| 42f95243d9 | |||
| e77ca7124e | |||
| 5c51822996 | |||
| 12fe94cbff | |||
| 4c4cfbb60e | |||
| 416f94484d | |||
| 52e79446f6 | |||
| 63d4af8543 | |||
| 0c6e74911d | |||
| c792bc25b6 | |||
| e1f8b8b7b7 | |||
| 7339cb9cc9 | |||
| 5c99da6617 | |||
| c22b54c898 | |||
| 8207a8f32a | |||
| 27cd80d508 | |||
| f6af235f51 |
@@ -0,0 +1,25 @@
|
||||
name: .NET
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --no-restore
|
||||
- name: Test
|
||||
run: dotnet test --no-build --verbosity normal
|
||||
@@ -0,0 +1,31 @@
|
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '33 20 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'No activity on this issue for 60 days. This issue will get closed if it receives no update within 14 more days.'
|
||||
stale-pr-message: 'No activity on this PR for 60 days. This PR will get closed if it receives no update within 14 more days.'
|
||||
close-issue-message: 'Closed for inactivity. Feel free to update this if the issue is still relevant.'
|
||||
close-pr-message: 'Closed for inactivity. Feel free to update this if the PR is still relevant.'
|
||||
exempt-issue-labels: 'Future'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
days-before-close: 14
|
||||
@@ -287,3 +287,5 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||
/Docs/*
|
||||
Docs/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
language: csharp
|
||||
mono: none
|
||||
solution: CryptoExchange.Net.sln
|
||||
dotnet: 5.0.103
|
||||
script:
|
||||
- dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.1"
|
||||
- dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj
|
||||
@@ -11,78 +11,6 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture()]
|
||||
public class BaseClientTests
|
||||
{
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", "")]
|
||||
[TestCase("test", null)]
|
||||
[TestCase("test", "")]
|
||||
[TestCase(null, "test")]
|
||||
[TestCase("", "test")]
|
||||
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) }));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingLogOutput_Should_RedirectLogOutput()
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new RestClientOptions("")
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
// act
|
||||
client.Log(LogLevel.Information, "Test");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
|
||||
}
|
||||
|
||||
[TestCase(LogLevel.None, LogLevel.Error, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
|
||||
[TestCase(null, LogLevel.Error, true)]
|
||||
[TestCase(null, LogLevel.Warning, true)]
|
||||
[TestCase(null, LogLevel.Information, true)]
|
||||
[TestCase(null, LogLevel.Debug, true)]
|
||||
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new RestClientOptions("")
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger },
|
||||
LogLevel = verbosity
|
||||
});
|
||||
|
||||
// act
|
||||
client.Log(testVerbosity, "Test");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
{
|
||||
@@ -90,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123}");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -103,24 +31,24 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void FillingPathParameters_Should_ResultInValidUrl()
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1/", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api/", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
|
||||
[TestCase("https://api.test.com/", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
|
||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.FillParameters("http://test.api/{}/path/{}", "1", "test");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result == "http://test.api/1/path/test");
|
||||
var result = baseUrl.AppendPath(path);
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
internal class CallResultTests
|
||||
{
|
||||
[Test]
|
||||
public void TestBasicErrorCallResult()
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicSuccessCallResult()
|
||||
{
|
||||
var result = new CallResult(null);
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultError()
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsNull(result.Data);
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccess()
|
||||
{
|
||||
var result = new CallResult<object>(new object());
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsNotNull(result.Data);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccessAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult.Data is TestObject2);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAsError()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultErrorAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsSuccess()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObjectResult
|
||||
{
|
||||
public TestObject2 InnerData;
|
||||
|
||||
public TestObjectResult()
|
||||
{
|
||||
InnerData = new TestObject2();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObject2
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class ConverterTests
|
||||
{
|
||||
[TestCase("2021-05-12")]
|
||||
[TestCase("20210512")]
|
||||
[TestCase("210512")]
|
||||
[TestCase("1620777600.000")]
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||
Assert.AreEqual(output.Time, expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600.000)]
|
||||
[TestCase(1620777600000d)]
|
||||
public void TestDateTimeConverterDouble(double input)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000000)]
|
||||
[TestCase(1620777600000000000)]
|
||||
[TestCase(0, true)]
|
||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.AreEqual(output.Time, expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600.000)]
|
||||
public void TestDateTimeConverterFromSeconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToSeconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000.000)]
|
||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMilliseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000)]
|
||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMicroseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000000)]
|
||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToNanoseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000000000);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void TestDateTimeConverterNull()
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
|
||||
Assert.AreEqual(output.Time, null);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.AreEqual(output, expected);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.AreEqual(output, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", null)]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", TestEnum.One)]
|
||||
[TestCase(null, TestEnum.One)]
|
||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
}
|
||||
}
|
||||
|
||||
public class TimeObject
|
||||
{
|
||||
[JsonConverter(typeof(DateTimeConverter))]
|
||||
public DateTime? Time { get; set; }
|
||||
}
|
||||
|
||||
public class EnumObject
|
||||
{
|
||||
public TestEnum? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableEnumObject
|
||||
{
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public enum TestEnum
|
||||
{
|
||||
[Map("1")]
|
||||
One,
|
||||
[Map("2")]
|
||||
Two,
|
||||
[Map("three", "3")]
|
||||
Three,
|
||||
Four
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"></packagereference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
|
||||
<packagereference Include="NUnit3TestAdapter" Version="3.17.0"></packagereference>
|
||||
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class OptionsTests
|
||||
{
|
||||
[TearDown]
|
||||
public void Init()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", "")]
|
||||
[TestCase("test", null)]
|
||||
[TestCase("test", "")]
|
||||
[TestCase(null, "test")]
|
||||
[TestCase("", "test")]
|
||||
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException),
|
||||
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApiOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(options => {
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
});
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient();
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
{
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient(options =>
|
||||
{
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||
});
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "444");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </summary>
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||
{
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
{
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,10 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetResponse("{\"property\": 123", out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -50,14 +50,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorCode_Should_ResultInError()
|
||||
public async Task ReceivingErrorCode_Should_ResultInError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -65,14 +65,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -83,14 +83,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
{
|
||||
// arrange
|
||||
var client = new ParseErrorTestRestClient();
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Api2.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
@@ -105,20 +105,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter>{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))},
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
|
||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||
});
|
||||
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
|
||||
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
|
||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
|
||||
Assert.IsTrue(client.RateLimiters.Count() == 1);
|
||||
Assert.IsTrue(client.RateLimitBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(client.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@@ -132,16 +128,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
});
|
||||
var client = new TestRestClient();
|
||||
|
||||
client.SetParameterPosition(new HttpMethod(method), pos);
|
||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
||||
|
||||
client.SetResponse("{}", out var request);
|
||||
|
||||
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
{
|
||||
{ "TestParam1", "Value1" },
|
||||
{ "TestParam2", 2 },
|
||||
@@ -161,84 +154,172 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests()
|
||||
|
||||
[TestCase(1, 0.1)]
|
||||
[TestCase(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
|
||||
// act
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsFalse(result2.Success);
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests()
|
||||
[TestCase("/sapi/test1", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/api/test1", false)]
|
||||
[TestCase("sapi/test1", false)]
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
}
|
||||
[TestCase("/sapi/", "/sapi/", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
|
||||
|
||||
// act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
sw.Stop();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsTrue(result2.Success);
|
||||
Assert.IsTrue(sw.ElapsedMilliseconds > 900, $"Actual: {sw.ElapsedMilliseconds}");
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey()
|
||||
[TestCase(1, 0.1)]
|
||||
[TestCase(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||
LogLevel = LogLevel.Debug,
|
||||
ApiCredentials = new ApiCredentials("TestKey", "TestSecret")
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||
}
|
||||
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
}
|
||||
|
||||
// act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetKey("TestKey2", "TestSecret2"); // set to different key
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
client.SetKey("TestKey", "TestSecret"); // set back to original key, should delay
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result3 = client.Request<TestObject>().Result;
|
||||
sw.Stop();
|
||||
[TestCase("/", false)]
|
||||
[TestCase("/sapi/test", true)]
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsTrue(result2.Success);
|
||||
Assert.IsTrue(result3.Success);
|
||||
Assert.IsTrue(sw.ElapsedMilliseconds > 900 && sw.ElapsedMilliseconds < 1900, $"Actual: {sw.ElapsedMilliseconds}");
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("/", false)]
|
||||
[TestCase("/sapi/test", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test1", "/api/test2", true)]
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", true, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, false, true)]
|
||||
[TestCase("/sapi/test", true, true, false, true)]
|
||||
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(new SocketClientOptions("")
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
options.SubOptions.MaxSocketConnections = 1;
|
||||
});
|
||||
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
|
||||
Assert.IsTrue(client.ReconnectInterval.TotalSeconds == 6);
|
||||
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
socket.CanConnect = canConnect;
|
||||
|
||||
//act
|
||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, socket));
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
@@ -49,20 +49,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.JsonData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
@@ -77,20 +79,23 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
options.SubOptions.OutputOriginalData = enabled;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
{
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
@@ -100,44 +105,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DisconnectedSocket_Should_Reconnect()
|
||||
{
|
||||
// arrange
|
||||
bool reconnected = false;
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
sub.ShouldReconnect = true;
|
||||
client.ConnectSocketSub(sub);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
sub.ConnectionRestored += (a) =>
|
||||
{
|
||||
reconnected = true;
|
||||
rstEvent.Set();
|
||||
};
|
||||
|
||||
// act
|
||||
socket.InvokeClose();
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(reconnected);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => {
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
client.ConnectSocketSub(sub);
|
||||
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||
var ups = new UpdateSubscription(sub, us);
|
||||
sub.AddSubscription(us);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAsync(ups).Wait();
|
||||
@@ -150,15 +131,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket1 = client.CreateSocket();
|
||||
var socket2 = client.CreateSocket();
|
||||
socket1.CanConnect = true;
|
||||
socket2.CanConnect = true;
|
||||
var sub1 = new SocketConnection(client, socket1);
|
||||
var sub2 = new SocketConnection(client, socket2);
|
||||
client.ConnectSocketSub(sub1);
|
||||
client.ConnectSocketSub(sub2);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||
client.SubClient.ConnectSocketSub(sub1);
|
||||
client.SubClient.ConnectSocketSub(sub2);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAllAsync().Wait();
|
||||
@@ -172,13 +153,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
|
||||
// act
|
||||
var connectResult = client.ConnectSocketSub(sub);
|
||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(connectResult.Success);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.OrderBook;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using NUnit.Framework;
|
||||
@@ -12,22 +14,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestFixture]
|
||||
public class SymbolOrderBookTests
|
||||
{
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions("Test", true, false);
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
public override void Dispose() {}
|
||||
|
||||
protected override Task<CallResult<bool>> DoResyncAsync()
|
||||
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
protected override Task<CallResult<UpdateSubscription>> DoStartAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -35,12 +37,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
||||
{
|
||||
Status = OrderBookStatus.Synced;
|
||||
base.bids.Clear();
|
||||
base._bids.Clear();
|
||||
foreach (var bid in bids)
|
||||
base.bids.Add(bid.Price, bid);
|
||||
base.asks.Clear();
|
||||
base._bids.Add(bid.Price, bid);
|
||||
base._asks.Clear();
|
||||
foreach (var ask in asks)
|
||||
base.asks.Add(ask.Price, ask);
|
||||
base._asks.Add(ask.Price, ask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,5 +110,33 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23333333m, resultAsks2.Data);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void CalculateTradableAmount()
|
||||
{
|
||||
var orderbook = new TestableSymbolOrderBook();
|
||||
orderbook.SetData(new List<ISymbolOrderBookEntry>
|
||||
{
|
||||
new BookEntry{ Price = 1, Quantity = 1 },
|
||||
new BookEntry{ Price = 1.1m, Quantity = 1 },
|
||||
},
|
||||
new List<ISymbolOrderBookEntry>()
|
||||
{
|
||||
new BookEntry{ Price = 1.2m, Quantity = 1 },
|
||||
new BookEntry{ Price = 1.3m, Quantity = 1 },
|
||||
});
|
||||
|
||||
var resultBids = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Bid);
|
||||
var resultAsks = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Ask);
|
||||
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.9m, resultBids.Data);
|
||||
Assert.AreEqual(1.61538462m, resultAsks.Data);
|
||||
Assert.AreEqual(1.4m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23076923m, resultAsks2.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public class TestBaseClient: BaseClient
|
||||
{
|
||||
public TestBaseClient(): base("Test", new RestClientOptions("http://testurl.url"), null)
|
||||
public TestSubClient SubClient { get; }
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public void Log(LogLevel verbosity, string data)
|
||||
{
|
||||
log.Write(verbosity, data);
|
||||
_logger.Log(verbosity, data);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubClient : RestApiClient
|
||||
{
|
||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
return Deserialize<T>(data, false);
|
||||
}
|
||||
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
|
||||
|
||||
public string FillParameters(string path, params string[] values)
|
||||
{
|
||||
return FillPathParameter(path, values);
|
||||
}
|
||||
public override TimeSpan? GetTimeOffset() => null;
|
||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
@@ -38,19 +53,19 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
|
||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
||||
{
|
||||
return base.AddAuthenticationToHeaders(uri, method, parameters, signed, postParameters, arraySerialization);
|
||||
}
|
||||
|
||||
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
return base.AddAuthenticationToParameters(uri, method, parameters, signed, postParameters, arraySerialization);
|
||||
bodyParameters = new SortedDictionary<string, object>();
|
||||
uriParameters = new SortedDictionary<string, object>();
|
||||
headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public override string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,29 +12,32 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestRestClient: RestClient
|
||||
public class TestRestClient: BaseRestClient
|
||||
{
|
||||
public TestRestClient() : base("Test", new RestClientOptions("http://testurl.url"), null)
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public TestRestClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
public void SetKey(string key, string secret)
|
||||
{
|
||||
SetAuthenticationProvider(new UnitTests.TestAuthProvider(new ApiCredentials(key, secret)));
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@@ -56,14 +59,23 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, string, int>((method, uri, id) =>
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(new Uri(uri));
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
requestObj = request.Object;
|
||||
}
|
||||
|
||||
@@ -73,10 +85,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
|
||||
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
@@ -98,13 +117,26 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, string, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(new Uri(uri)))
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
@@ -113,23 +145,70 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
}
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
{
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
protected override Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
public ParseErrorTestRestClient(RestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
|
||||
protected override Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public bool Connected { get; set; }
|
||||
|
||||
public event Action OnClose;
|
||||
|
||||
#pragma warning disable 0067
|
||||
public event Action OnReconnected;
|
||||
public event Action OnReconnecting;
|
||||
#pragma warning restore 0067
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
|
||||
public int Id { get; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
@@ -38,6 +44,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
public double IncomingKbps => throw new NotImplementedException();
|
||||
|
||||
public Uri Uri => new Uri("");
|
||||
|
||||
public TimeSpan KeepAliveInterval { get; set; }
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
|
||||
@@ -89,6 +99,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
Reconnecting = true;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
@@ -111,5 +122,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
public Task ReconnectAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: SocketClient
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSocketClient() : this(new SocketClientOptions("http://testurl.url"))
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(SocketClientOptions exchangeOptions) : base("test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
{
|
||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
return (TestSocket)CreateSocket(BaseAddress);
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
||||
{
|
||||
public static TestSocketOptions Default = new TestSocketOptions
|
||||
{
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal IWebsocket CreateSocketInternal(string address)
|
||||
{
|
||||
return CreateSocket(address);
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
@@ -43,12 +99,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(JToken message, object request)
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(JToken message, string identifier)
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestStringLogger : ILogger
|
||||
{
|
||||
StringBuilder _builder = new StringBuilder();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
_builder.AppendLine(formatter(state, exception));
|
||||
}
|
||||
|
||||
public string GetLogs()
|
||||
{
|
||||
return _builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -1,12 +1,18 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2008
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net.UnitTests", "CryptoExchange.Net.UnitTests\CryptoExchange.Net.UnitTests.csproj", "{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\BlazorClient\BlazorClient.csproj", "{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -21,10 +27,22 @@ Global
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
|
||||
/// <summary>
|
||||
/// Used for conversion in ArrayConverter
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class JsonConversionAttribute: Attribute
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks property as optional
|
||||
/// </summary>
|
||||
public class JsonOptionalPropertyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Map a enum entry to string values
|
||||
/// </summary>
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Values mapping to the enum entry
|
||||
/// </summary>
|
||||
public string[] Values { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="maps"></param>
|
||||
public MapAttribute(params string[] maps)
|
||||
{
|
||||
Values = maps;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Newtonsoft.Json.Linq;
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials info
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials: IDisposable
|
||||
{
|
||||
@@ -22,17 +22,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
public SecureString? Secret { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key to authenticate requests
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public PrivateKey? PrivateKey { get; }
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing a private key for authentication
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key used for signing</param>
|
||||
public ApiCredentials(PrivateKey privateKey)
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,11 +40,13 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret)
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
@@ -54,11 +56,22 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(string key, string secret)
|
||||
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
}
|
||||
@@ -67,12 +80,10 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// Copy the credentials
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ApiCredentials Copy()
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
if (PrivateKey == null)
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||
else
|
||||
return new ApiCredentials(PrivateKey!.Copy());
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,7 +133,6 @@ namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
Key?.Dispose();
|
||||
Secret?.Dispose();
|
||||
PrivateKey?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Credentials type
|
||||
/// </summary>
|
||||
public enum ApiCredentialsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hmac keys credentials
|
||||
/// </summary>
|
||||
Hmac,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in xml format
|
||||
/// </summary>
|
||||
RsaXml,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||
/// </summary>
|
||||
RsaPem
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for authentication providers
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The provided credentials
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
public ApiCredentials Credentials { get; }
|
||||
protected readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -20,39 +30,189 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
Credentials = credentials;
|
||||
if (credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add authentication to the parameter list based on the provided credentials
|
||||
/// Authenticate a request. Output parameters should include the providedParameters input
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri the request is for</param>
|
||||
/// <param name="method">The HTTP method of the request</param>
|
||||
/// <param name="parameters">The provided parameters for the request</param>
|
||||
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
|
||||
/// <param name="parameterPosition">Where parameters are placed, in the URI or in the request body</param>
|
||||
/// <param name="arraySerialization">How array parameters are serialized</param>
|
||||
/// <returns>Should return the original parameter list including any authentication parameters needed</returns>
|
||||
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
|
||||
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for the request</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="providedParameters">The request parameters</param>
|
||||
/// <param name="auth">If the requests should be authenticated</param>
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||
/// <param name="headers">The headers that should be send with the request</param>
|
||||
public abstract void AuthenticateRequest(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object> providedParameters,
|
||||
bool auth,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
out SortedDictionary<string, object> uriParameters,
|
||||
out SortedDictionary<string, object> bodyParameters,
|
||||
out Dictionary<string, string> headers
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA256Bytes(string data)
|
||||
{
|
||||
return parameters;
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add authentication to the header dictionary based on the provided credentials
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri the request is for</param>
|
||||
/// <param name="method">The HTTP method of the request</param>
|
||||
/// <param name="parameters">The provided parameters for the request</param>
|
||||
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
|
||||
/// <param name="parameterPosition">Where post parameters are placed, in the URI or in the request body</param>
|
||||
/// <param name="arraySerialization">How array parameters are serialized</param>
|
||||
/// <returns>Should return a dictionary containing any header key/value pairs needed for authenticating the request</returns>
|
||||
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
|
||||
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA512(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!.GetString()
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
|
||||
key)
|
||||
, out _);
|
||||
#else
|
||||
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
|
||||
#endif
|
||||
}
|
||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||
{
|
||||
// Read from xml private key format
|
||||
rsa.FromXmlString(_credentials.Secret!.GetString());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,16 +236,67 @@ namespace CryptoExchange.Net.Authentication
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to hex
|
||||
/// Convert byte array to hex string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string ByteToString(byte[] buff)
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2"); /* hex format */
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to base64 string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string BytesToBase64String(byte[] buff)
|
||||
{
|
||||
return Convert.ToBase64String(buff);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current timestamp including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key info
|
||||
/// </summary>
|
||||
public class PrivateKey : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The private key
|
||||
/// </summary>
|
||||
public SecureString Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key's pass phrase
|
||||
/// </summary>
|
||||
public SecureString? Passphrase { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the private key is encrypted or not
|
||||
/// </summary>
|
||||
public bool IsEncrypted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(SecureString key, SecureString passphrase)
|
||||
{
|
||||
Key = key;
|
||||
Passphrase = passphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(string key, string passphrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
|
||||
throw new ArgumentException("Key and passphrase can't be null/empty");
|
||||
|
||||
var secureKey = new SecureString();
|
||||
foreach (var c in key)
|
||||
secureKey.AppendChar(c);
|
||||
secureKey.MakeReadOnly();
|
||||
Key = secureKey;
|
||||
|
||||
var securePassphrase = new SecureString();
|
||||
foreach (var c in passphrase)
|
||||
securePassphrase.AppendChar(c);
|
||||
securePassphrase.MakeReadOnly();
|
||||
Passphrase = securePassphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an unencrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(SecureString key)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentException("Key can't be null/empty");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the private key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PrivateKey Copy()
|
||||
{
|
||||
if (Passphrase == null)
|
||||
return new PrivateKey(Key.GetString());
|
||||
else
|
||||
return new PrivateKey(Key.GetString(), Passphrase.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Passphrase?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Output string type
|
||||
/// </summary>
|
||||
public enum SignOutputType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hex string
|
||||
/// </summary>
|
||||
Hex,
|
||||
/// <summary>
|
||||
/// Base64 string
|
||||
/// </summary>
|
||||
Base64
|
||||
}
|
||||
}
|
||||
@@ -1,463 +0,0 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress { get; }
|
||||
/// <summary>
|
||||
/// The name of the exchange the client is for
|
||||
/// </summary>
|
||||
public string ExchangeName { get; }
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal Log log;
|
||||
/// <summary>
|
||||
/// The api proxy
|
||||
/// </summary>
|
||||
protected ApiProxy? apiProxy;
|
||||
/// <summary>
|
||||
/// The authentication provider
|
||||
/// </summary>
|
||||
protected internal AuthenticationProvider? authProvider;
|
||||
/// <summary>
|
||||
/// Should check objects for missing properties based on the model and the received JSON
|
||||
/// </summary>
|
||||
public bool ShouldCheckObjects { get; set; }
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; private set; }
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
protected static int lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
protected static object idLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Last id used
|
||||
/// </summary>
|
||||
public static int LastId => lastId;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="exchangeName">The name of the exchange this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
|
||||
protected BaseClient(string exchangeName, ClientOptions options, AuthenticationProvider? authenticationProvider)
|
||||
{
|
||||
log = new Log(exchangeName);
|
||||
authProvider = authenticationProvider;
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
|
||||
ExchangeName = exchangeName;
|
||||
OutputOriginalData = options.OutputOriginalData;
|
||||
BaseAddress = options.BaseAddress;
|
||||
apiProxy = options.Proxy;
|
||||
|
||||
log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
ShouldCheckObjects = options.ShouldCheckObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the authentication provider, can be used when manually setting the API credentials
|
||||
/// </summary>
|
||||
/// <param name="authenticationProvider"></param>
|
||||
protected void SetAuthenticationProvider(AuthenticationProvider authenticationProvider)
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Setting api credentials");
|
||||
authProvider = authenticationProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and returns a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(default, tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, checkObject, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
if ((checkObject ?? ShouldCheckObjects)&& log.Level <= LogLevel.Debug)
|
||||
{
|
||||
// This checks the input JToken object against the class it is being serialized into and outputs any missing fields
|
||||
// in either the input or the class
|
||||
try
|
||||
{
|
||||
if (obj is JObject o)
|
||||
{
|
||||
CheckObject(typeof(T), o, requestId);
|
||||
}
|
||||
else if (obj is JArray j)
|
||||
{
|
||||
if (j.HasValues && j[0] is JObject jObject)
|
||||
CheckObject(typeof(T).GetElementType(), jObject, requestId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(default, new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(default, new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(default, new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (OutputOriginalData || log.Level <= LogLevel.Debug)
|
||||
{
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}");
|
||||
var result = Deserialize<T>(data, null, serializer, requestId);
|
||||
if(OutputOriginalData)
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek) {
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void CheckObject(Type type, JObject obj, int? requestId = null)
|
||||
{
|
||||
if (type == null)
|
||||
return;
|
||||
|
||||
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
|
||||
// If type has a custom JsonConverter we assume this will handle property mapping
|
||||
return;
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
return;
|
||||
|
||||
if (!obj.HasValues && type != typeof(object))
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var isDif = false;
|
||||
var properties = new List<string>();
|
||||
var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
|
||||
if (ignore != null)
|
||||
continue;
|
||||
|
||||
var propertyName = ((JsonPropertyAttribute?) attr)?.PropertyName;
|
||||
properties.Add(propertyName ?? prop.Name);
|
||||
}
|
||||
foreach (var token in obj)
|
||||
{
|
||||
var d = properties.FirstOrDefault(p => p == token.Key);
|
||||
if (d == null)
|
||||
{
|
||||
d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase));
|
||||
if (d == null)
|
||||
{
|
||||
if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
|
||||
isDif = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
properties.Remove(d);
|
||||
|
||||
var propType = GetProperty(d, props)?.PropertyType;
|
||||
if (propType == null || token.Value == null)
|
||||
continue;
|
||||
if (!IsSimple(propType) && propType != typeof(DateTime))
|
||||
{
|
||||
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
|
||||
CheckObject(propType.GetElementType()!, (JObject)token.Value[0]!, requestId);
|
||||
else if (token.Value is JObject o)
|
||||
CheckObject(propType, o, requestId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var propInfo = props.First(p => p.Name == prop ||
|
||||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
|
||||
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
|
||||
if (optional != null)
|
||||
continue;
|
||||
|
||||
isDif = true;
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
|
||||
}
|
||||
|
||||
if (isDif)
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj);
|
||||
}
|
||||
|
||||
private static PropertyInfo? GetProperty(string name, IEnumerable<PropertyInfo> props)
|
||||
{
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
if (attr == null)
|
||||
{
|
||||
if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase))
|
||||
return prop;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (((JsonPropertyAttribute)attr).PropertyName == name)
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int NextId()
|
||||
{
|
||||
lock (idLock)
|
||||
{
|
||||
lastId += 1;
|
||||
return lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
protected static string FillPathParameter(string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
authProvider?.Credentials?.Dispose();
|
||||
log.Write(LogLevel.Debug, "Disposing exchange client");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// If we are disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// </summary>
|
||||
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
|
||||
{
|
||||
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
||||
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Request body content type
|
||||
/// </summary>
|
||||
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not we need to manually parse an error instead of relying on the http status code
|
||||
/// </summary>
|
||||
public bool manualParseError = false;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
public string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
public string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Output the original string data along with the deserialized object
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
protected static int _lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
protected static object _idLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
public ApiOptions ApiOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Options
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
/// <param name="apiOptions">Api options</param>
|
||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
ClientOptions = clientOptions;
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
if (credentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data));
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
string? data = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (OutputOriginalData == true)
|
||||
{
|
||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
|
||||
var result = Deserialize<T>(data, serializer, requestId);
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected static int NextId()
|
||||
{
|
||||
lock (_idLock)
|
||||
{
|
||||
_lastId += 1;
|
||||
return _lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
internal string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string name)
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the client with the specified options
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public virtual void Initialize(ExchangeOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register an API client
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||
{
|
||||
if (ClientOptions == null)
|
||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||
|
||||
_logger.Log(LogLevel.Trace, $" {apiClient.GetType().Name}, base address: {apiClient.BaseAddress}");
|
||||
ApiClients.Add(apiClient);
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest client
|
||||
/// </summary>
|
||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for socket client implementations
|
||||
/// </summary>
|
||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||
{
|
||||
#region fields
|
||||
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
foreach(var socket in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||
if (result)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
tasks.Add(client.UnsubscribeAllAsync());
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach(var client in ApiClients.OfType<SocketApiClient>())
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client for interacting with a REST API
|
||||
/// </summary>
|
||||
public abstract class RestApiClient : BaseApiClient, IRestApiClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of rate limiters
|
||||
/// </summary>
|
||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="httpClient">HttpClient to use</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
var rateLimiters = new List<IRateLimiter>();
|
||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||
rateLimiters.Add(rateLimiter);
|
||||
RateLimiters = rateLimiters;
|
||||
|
||||
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a request to the uri and returns if it was successful
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult> SendRequestAsync(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
int currentTry = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
return result.AsDataless();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
[return: NotNull]
|
||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false
|
||||
) where T : class
|
||||
{
|
||||
int currentTry = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||
if (!request)
|
||||
return new WebCallResult<T>(request.Error!);
|
||||
|
||||
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares a request to be sent to the server
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||
/// <param name="requestWeight">Credits used for the request</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, object>? parameters = null,
|
||||
bool signed = false,
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
int requestWeight = 1,
|
||||
JsonSerializer? deserializer = null,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
bool ignoreRatelimit = false)
|
||||
{
|
||||
var requestId = NextId();
|
||||
|
||||
if (signed)
|
||||
{
|
||||
var syncTask = SyncTimeAsync();
|
||||
var timeSyncInfo = GetTimeSyncInfo();
|
||||
|
||||
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||
{
|
||||
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||
var syncTimeResult = await syncTask.ConfigureAwait(false);
|
||||
if (!syncTimeResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||
return syncTimeResult.As<IRequest>(default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreRatelimit)
|
||||
{
|
||||
foreach (var limiter in RateLimiters)
|
||||
{
|
||||
var limitResult = await limiter.LimitRequestAsync(_logger, uri.AbsolutePath, method, signed, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, ApiOptions.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult.Success)
|
||||
return new CallResult<IRequest>(limitResult.Error!);
|
||||
}
|
||||
}
|
||||
|
||||
if (signed && AuthenticationProvider == null)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||
|
||||
string? paramString = "";
|
||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||
paramString = $" with request body '{request.Content}'";
|
||||
|
||||
var headers = request.GetHeaders();
|
||||
if (headers.Any())
|
||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||
|
||||
TotalRequestsMade++;
|
||||
_logger.Log(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
|
||||
return new CallResult<IRequest>(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request and returns the result deserialized into the type parameter class
|
||||
/// </summary>
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||
IRequest request,
|
||||
JsonSerializer? deserializer,
|
||||
CancellationToken cancellationToken,
|
||||
bool expectedEmptyResponse)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
var statusCode = response.StatusCode;
|
||||
var headers = response.ResponseHeaders;
|
||||
var responseLength = response.ContentLength;
|
||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
||||
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
||||
if (manualParseError)
|
||||
{
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
responseLength ??= data.Length;
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
_logger.Log(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(OutputOriginalData ? (": " + data) : "")}");
|
||||
|
||||
if (!expectedEmptyResponse)
|
||||
{
|
||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
// Let the library implementation see if it is an error response, and if so parse the error
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
|
||||
// Not an error, so continue deserializing
|
||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data))
|
||||
{
|
||||
var parseResult = ValidateJson(data);
|
||||
if (!parseResult.Success)
|
||||
// Not empty, and not json
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||
|
||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
// Error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||
}
|
||||
|
||||
// Empty success response; okay
|
||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expectedEmptyResponse)
|
||||
{
|
||||
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||
}
|
||||
|
||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, responseLength, OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Http status code indicates error
|
||||
using var reader = new StreamReader(responseStream);
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||
responseStream.Close();
|
||||
response.Close();
|
||||
var parseResult = ValidateJson(data);
|
||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||
if (error.Code == null || error.Code == 0)
|
||||
error.Code = (int)response.StatusCode;
|
||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var exceptionInfo = requestException.ToLogString();
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||
return new WebCallResult<T>(null, null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
|
||||
/// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not.
|
||||
/// If the response is an error this method should return the parsed error, else it should return null
|
||||
/// </summary>
|
||||
/// <param name="data">Received data</param>
|
||||
/// <returns>Null if not an error, Error otherwise</returns>
|
||||
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
||||
{
|
||||
return Task.FromResult<ServerError?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||
/// Note that this is always called; even when the request might be successful
|
||||
/// </summary>
|
||||
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
||||
/// <param name="callResult">The result of the call</param>
|
||||
/// <param name="tries">The current try number</param>
|
||||
/// <returns>True if call should retry, false if the call should return</returns>
|
||||
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request object
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri to send the request to</param>
|
||||
/// <param name="method">The method of the request</param>
|
||||
/// <param name="parameters">The parameters of the request</param>
|
||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
||||
/// <param name="requestId">Unique id of a request</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest ConstructRequest(
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object>? parameters,
|
||||
bool signed,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
int requestId,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
parameters ??= new Dictionary<string, object>();
|
||||
|
||||
for (var i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var kvp = parameters.ElementAt(i);
|
||||
if (kvp.Value is Func<object> delegateValue)
|
||||
parameters[kvp.Key] = delegateValue();
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||
{
|
||||
foreach (var parameter in parameters)
|
||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||
if (AuthenticationProvider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
AuthenticationProvider.AuthenticateRequest(
|
||||
this,
|
||||
uri,
|
||||
method,
|
||||
parameters,
|
||||
signed,
|
||||
arraySerialization,
|
||||
parameterPosition,
|
||||
out uriParameters,
|
||||
out bodyParameters,
|
||||
out headers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||
{
|
||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||
$"should return provided parameters in either the uri or body parameters output");
|
||||
}
|
||||
}
|
||||
|
||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||
|
||||
var request = RequestFactory.Create(method, uri, requestId);
|
||||
request.Accept = Constants.JsonContentHeader;
|
||||
|
||||
foreach (var header in headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
foreach (var header in additionalHeaders)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (StandardRequestHeaders != null)
|
||||
{
|
||||
foreach (var header in StandardRequestHeaders)
|
||||
{
|
||||
// Only add it if it isn't overwritten
|
||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
if (bodyParameters.Any())
|
||||
WriteParamBody(request, bodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(requestBodyEmptyContent, contentType);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the parameters of the request to the request object body
|
||||
/// </summary>
|
||||
/// <param name="request">The request to set the parameters on</param>
|
||||
/// <param name="parameters">The parameters to set</param>
|
||||
/// <param name="contentType">The content type of the data</param>
|
||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||
{
|
||||
if (requestBodyFormat == RequestBodyFormat.Json)
|
||||
{
|
||||
// Write the parameters as json in the body
|
||||
var stringData = JsonConvert.SerializeObject(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (requestBodyFormat == RequestBodyFormat.FormData)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
||||
/// </summary>
|
||||
/// <param name="error">The string the request returned</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError(error.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
/// <returns>Server time</returns>
|
||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
|
||||
internal async Task<WebCallResult<bool>> SyncTimeAsync()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
var result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return result.As(false);
|
||||
}
|
||||
|
||||
if (TotalRequestsMade == 1)
|
||||
{
|
||||
// If this was the first request make another one to calculate the offset since the first one can be slower
|
||||
localTime = DateTime.UtcNow;
|
||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return result.As(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate time offset between local and server
|
||||
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
|
||||
timeSyncParams.UpdateTimeOffset(offset);
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
+318
-178
@@ -1,85 +1,68 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for socket client implementations
|
||||
/// Base socket API client for interaction with a websocket API
|
||||
/// </summary>
|
||||
public abstract class SocketClient: BaseClient, ISocketClient
|
||||
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
|
||||
{
|
||||
#region fields
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
#region Fields
|
||||
/// <inheritdoc/>
|
||||
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
|
||||
|
||||
/// <summary>
|
||||
/// List of socket connections currently connecting/connected
|
||||
/// </summary>
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> sockets = new ConcurrentDictionary<int, SocketConnection>();
|
||||
protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
|
||||
|
||||
/// <summary>
|
||||
/// Semaphore used while creating sockets
|
||||
/// </summary>
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
|
||||
protected internal readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.ReconnectInterval"/>
|
||||
public TimeSpan ReconnectInterval { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.AutoReconnect"/>
|
||||
public bool AutoReconnect { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
|
||||
public TimeSpan ResponseTimeout { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
|
||||
public TimeSpan SocketNoDataTimeout { get; }
|
||||
/// <summary>
|
||||
/// The max amount of concurrent socket connections
|
||||
/// Keep alive interval for websocket connection
|
||||
/// </summary>
|
||||
public int MaxSocketConnections { get; protected set; } = 9999;
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
|
||||
public int SocketCombineTarget { get; protected set; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxReconnectTries"/>
|
||||
public int? MaxReconnectTries { get; protected set; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxResubscribeTries"/>
|
||||
public int? MaxResubscribeTries { get; protected set; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxConcurrentResubscriptionsPerSocket"/>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; protected set; }
|
||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<byte[], string>? dataInterpreterBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used for processing string data received from socket connections before it is processed by handlers
|
||||
/// </summary>
|
||||
protected Func<string, string>? dataInterpreterString;
|
||||
|
||||
/// <summary>
|
||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||
/// </summary>
|
||||
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new Dictionary<string, Action<MessageEvent>>();
|
||||
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
|
||||
|
||||
/// <summary>
|
||||
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
|
||||
/// </summary>
|
||||
protected Task? periodicTask;
|
||||
|
||||
/// <summary>
|
||||
/// Wait event for the periodicTask
|
||||
/// </summary>
|
||||
protected AsyncResetEvent? periodicEvent;
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool disposing;
|
||||
|
||||
/// <summary>
|
||||
/// If true; data which is a response to a query will also be distributed to subscriptions
|
||||
@@ -97,40 +80,56 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!sockets.Any())
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return sockets.Sum(s => s.Value.Socket.IncomingKbps);
|
||||
return socketConnections.Sum(s => s.Value.IncomingKbps);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => socketConnections.Count;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!socketConnections.Any())
|
||||
return 0;
|
||||
|
||||
return socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="exchangeName">The name of the exchange this client is for</param>
|
||||
/// <param name="exchangeOptions">The options for this client</param>
|
||||
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
|
||||
protected SocketClient(string exchangeName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeName, exchangeOptions, authenticationProvider)
|
||||
/// <param name="logger">log</param>
|
||||
/// <param name="options">Client options</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
if (exchangeOptions == null)
|
||||
throw new ArgumentNullException(nameof(exchangeOptions));
|
||||
|
||||
AutoReconnect = exchangeOptions.AutoReconnect;
|
||||
ReconnectInterval = exchangeOptions.ReconnectInterval;
|
||||
ResponseTimeout = exchangeOptions.SocketResponseTimeout;
|
||||
SocketNoDataTimeout = exchangeOptions.SocketNoDataTimeout;
|
||||
SocketCombineTarget = exchangeOptions.SocketSubscriptionsCombineTarget ?? 1;
|
||||
MaxReconnectTries = exchangeOptions.MaxReconnectTries;
|
||||
MaxResubscribeTries = exchangeOptions.MaxResubscribeTries;
|
||||
MaxConcurrentResubscriptionsPerSocket = exchangeOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -152,10 +151,11 @@ namespace CryptoExchange.Net
|
||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||
/// <param name="dataHandler">The handler of update data</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
|
||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler);
|
||||
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,48 +167,72 @@ namespace CryptoExchange.Net
|
||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||
/// <param name="dataHandler">The handler of update data</param>
|
||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler)
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
SocketSubscription subscription;
|
||||
SocketSubscription? subscription;
|
||||
var released = false;
|
||||
// Wait for a semaphore here, so we only connect 1 socket at a time.
|
||||
// This is necessary for being able to see if connections can be combined
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
socketConnection = GetSocketConnection(url, authenticated);
|
||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler);
|
||||
if (SocketCombineTarget == 1)
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
// Add a subscription on the socket connection
|
||||
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
|
||||
if (subscription == null)
|
||||
{
|
||||
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
|
||||
semaphoreSlim.Release();
|
||||
released = true;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(connectResult.Error!);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<UpdateSubscription>(null, connectResult.Error);
|
||||
|
||||
if (needsConnecting)
|
||||
log.Write(LogLevel.Debug, $"Socket {socketConnection.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(!released)
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} has been paused, can't subscribe at this moment");
|
||||
return new CallResult<UpdateSubscription>(default, new ServerError("Socket is paused"));
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
|
||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
if (request != null)
|
||||
@@ -217,8 +241,9 @@ namespace CryptoExchange.Net
|
||||
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(null, subResult.Error);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -227,8 +252,17 @@ namespace CryptoExchange.Net
|
||||
subscription.Confirmed = true;
|
||||
}
|
||||
|
||||
socketConnection.ShouldReconnect = true;
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription), null);
|
||||
if (ct != default)
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
}, false);
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
|
||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -241,12 +275,18 @@ namespace CryptoExchange.Net
|
||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||
{
|
||||
CallResult<object>? callResult = null;
|
||||
await socketConnection.SendAndWaitAsync(request, ResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||
|
||||
if (callResult?.Success == true)
|
||||
{
|
||||
subscription.Confirmed = true;
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
return new CallResult<bool>(callResult?.Success ?? false, callResult == null ? new ServerError("No response on subscription request received"): callResult.Error);
|
||||
if (callResult == null)
|
||||
return new CallResult<bool>(new ServerError("No response on subscription request received"));
|
||||
|
||||
return new CallResult<bool>(callResult.Error!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -271,13 +311,21 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
|
||||
{
|
||||
if (_disposing)
|
||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
||||
|
||||
SocketConnection socketConnection;
|
||||
var released = false;
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
socketConnection = GetSocketConnection(url, authenticated);
|
||||
if (SocketCombineTarget == 1)
|
||||
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<T>(default);
|
||||
|
||||
socketConnection = socketResult.Data;
|
||||
|
||||
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
|
||||
{
|
||||
// Can release early when only a single sub per connection
|
||||
semaphoreSlim.Release();
|
||||
@@ -286,20 +334,18 @@ namespace CryptoExchange.Net
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<T>(default, connectResult.Error);
|
||||
return new CallResult<T>(connectResult.Error!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked.
|
||||
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution
|
||||
if (!released)
|
||||
semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
if (socketConnection.PausedActivity)
|
||||
{
|
||||
log.Write(LogLevel.Information, $"Socket {socketConnection.Socket.Id} has been paused, can't send query at this moment");
|
||||
return new CallResult<T>(default, new ServerError("Socket is paused"));
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
|
||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||
}
|
||||
|
||||
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
||||
@@ -314,8 +360,8 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
||||
{
|
||||
var dataResult = new CallResult<T>(default, new ServerError("No response on query received"));
|
||||
await socket.SendAndWaitAsync(request, ResponseTimeout, data =>
|
||||
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, data =>
|
||||
{
|
||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||
return false;
|
||||
@@ -336,25 +382,32 @@ namespace CryptoExchange.Net
|
||||
protected virtual async Task<CallResult<bool>> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!connectResult)
|
||||
return new CallResult<bool>(false, connectResult.Error);
|
||||
return new CallResult<bool>(connectResult.Error!);
|
||||
|
||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
|
||||
_logger.Log(LogLevel.Debug, $"Attempting to authenticate {socket.SocketId}");
|
||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} authentication failed");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
|
||||
if (socket.Connected)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
result.Error!.Message = "Authentication failed: " + result.Error.Message;
|
||||
return new CallResult<bool>(false, result.Error);
|
||||
return new CallResult<bool>(result.Error);
|
||||
}
|
||||
|
||||
socket.Authenticated = true;
|
||||
return new CallResult<bool>(true, null);
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -370,7 +423,8 @@ namespace CryptoExchange.Net
|
||||
/// <param name="data">The message received from the server</param>
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the query</returns>
|
||||
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)]out CallResult<T>? callResult);
|
||||
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)] out CallResult<T>? callResult);
|
||||
|
||||
/// <summary>
|
||||
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter).
|
||||
/// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
|
||||
@@ -385,28 +439,34 @@ namespace CryptoExchange.Net
|
||||
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
|
||||
/// <returns>True if the message was a response to the subscription request</returns>
|
||||
protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
|
||||
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
||||
/// <param name="message">The received data</param>
|
||||
/// <param name="request">The subscription request</param>
|
||||
/// <returns>True if the message is for the subscription which sent the request</returns>
|
||||
protected internal abstract bool MessageMatchesHandler(JToken message, object request);
|
||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
|
||||
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection the message was recieved on</param>
|
||||
/// <param name="message">The received data</param>
|
||||
/// <param name="identifier">The string identifier of the handler</param>
|
||||
/// <returns>True if the message is for the handler which has the identifier</returns>
|
||||
protected internal abstract bool MessageMatchesHandler(JToken message, string identifier);
|
||||
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
|
||||
/// </summary>
|
||||
/// <param name="socketConnection">The socket connection that should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected internal abstract Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socketConnection);
|
||||
|
||||
/// <summary>
|
||||
/// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway
|
||||
/// </summary>
|
||||
@@ -434,8 +494,9 @@ namespace CryptoExchange.Net
|
||||
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
|
||||
/// <param name="connection">The socket connection the handler is on</param>
|
||||
/// <param name="dataHandler">The handler of the data received</param>
|
||||
/// <param name="authenticated">Whether the subscription needs authentication</param>
|
||||
/// <returns></returns>
|
||||
protected virtual SocketSubscription AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler)
|
||||
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
|
||||
{
|
||||
void InternalHandler(MessageEvent messageEvent)
|
||||
{
|
||||
@@ -446,10 +507,10 @@ namespace CryptoExchange.Net
|
||||
return;
|
||||
}
|
||||
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData, false);
|
||||
var desResult = Deserialize<T>(messageEvent.JsonData);
|
||||
if (!desResult)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {connection.Socket.Id} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||
_logger.Log(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -457,9 +518,10 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
var subscription = request == null
|
||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, InternalHandler)
|
||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, InternalHandler);
|
||||
connection.AddSubscription(subscription);
|
||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||
if (!connection.AddSubscription(subscription))
|
||||
return null;
|
||||
return subscription;
|
||||
}
|
||||
|
||||
@@ -467,46 +529,89 @@ namespace CryptoExchange.Net
|
||||
/// Adds a generic message handler. Used for example to reply to ping requests
|
||||
/// </summary>
|
||||
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
|
||||
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(Newtonsoft.Json.Linq.JToken,string)"/>)</param>
|
||||
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
|
||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
||||
{
|
||||
genericHandlers.Add(identifier, action);
|
||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, action);
|
||||
foreach (var connection in sockets.Values)
|
||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
|
||||
foreach (var connection in socketConnections.Values)
|
||||
connection.AddSubscription(subscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
||||
/// </summary>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="authentication"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
|
||||
{
|
||||
return Task.FromResult(new CallResult<string?>(address));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to reconnect to after losing a connection
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||
{
|
||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||
/// </summary>
|
||||
/// <param name="request">The original request</param>
|
||||
/// <returns></returns>
|
||||
public virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
|
||||
{
|
||||
return Task.FromResult(new CallResult<object>(request));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <returns></returns>
|
||||
protected virtual SocketConnection GetSocketConnection(string address, bool authenticated)
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
||||
{
|
||||
var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/')
|
||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& (s.Value.ApiClient.GetType() == GetType())
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||
if (result != null)
|
||||
{
|
||||
if (result.SubscriptionCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= SocketCombineTarget)))
|
||||
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
return result;
|
||||
return new CallResult<SocketConnection>(result);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
if (!connectionAddress)
|
||||
{
|
||||
_logger.Log(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||
return connectionAddress.As<SocketConnection>(null);
|
||||
}
|
||||
|
||||
if (connectionAddress.Data != address)
|
||||
_logger.Log(LogLevel.Debug, $"Connection address set to " + connectionAddress.Data);
|
||||
|
||||
// Create new socket
|
||||
var socket = CreateSocket(address);
|
||||
var socketConnection = new SocketConnection(this, socket);
|
||||
var socket = CreateSocket(connectionAddress.Data!);
|
||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
foreach (var kvp in genericHandlers)
|
||||
{
|
||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, kvp.Value);
|
||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
|
||||
socketConnection.AddSubscription(handler);
|
||||
}
|
||||
|
||||
return socketConnection;
|
||||
return new CallResult<SocketConnection>(socketConnection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -524,16 +629,33 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<bool>> ConnectSocketAsync(SocketConnection socketConnection)
|
||||
{
|
||||
if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false))
|
||||
if (await socketConnection.ConnectAsync().ConfigureAwait(false))
|
||||
{
|
||||
sockets.TryAdd(socketConnection.Socket.Id, socketConnection);
|
||||
return new CallResult<bool>(true, null);
|
||||
socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
|
||||
return new CallResult<bool>(true);
|
||||
}
|
||||
|
||||
socketConnection.Socket.Dispose();
|
||||
return new CallResult<bool>(false, new CantConnectError());
|
||||
socketConnection.Dispose();
|
||||
return new CallResult<bool>(new CantConnectError());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get parameters for the websocket connection
|
||||
/// </summary>
|
||||
/// <param name="address">The address to connect to</param>
|
||||
/// <returns></returns>
|
||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
||||
{
|
||||
DataInterpreterBytes = dataInterpreterBytes,
|
||||
DataInterpreterString = dataInterpreterString,
|
||||
KeepAliveInterval = KeepAliveInterval,
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Create a socket for an address
|
||||
/// </summary>
|
||||
@@ -541,32 +663,18 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected virtual IWebsocket CreateSocket(string address)
|
||||
{
|
||||
var socket = SocketFactory.CreateWebsocket(log, address);
|
||||
log.Write(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||
|
||||
if (apiProxy != null)
|
||||
socket.SetProxy(apiProxy);
|
||||
|
||||
socket.Timeout = SocketNoDataTimeout;
|
||||
socket.DataInterpreterBytes = dataInterpreterBytes;
|
||||
socket.DataInterpreterString = dataInterpreterString;
|
||||
socket.RatelimitPerSecond = RateLimitPerSocketPerSecond;
|
||||
socket.OnError += e =>
|
||||
{
|
||||
if(e is WebSocketException wse)
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: Websocket error code {wse.WebSocketErrorCode}, details: " + e.ToLogString());
|
||||
else
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Id} error: " + e.ToLogString());
|
||||
};
|
||||
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
|
||||
_logger.Log(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
|
||||
return socket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodically sends data over a socket connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">Identifier for the periodic send</param>
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
public virtual void SendPeriodic(TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
public virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
|
||||
{
|
||||
if (objGetter == null)
|
||||
throw new ArgumentNullException(nameof(objGetter));
|
||||
@@ -574,33 +682,33 @@ namespace CryptoExchange.Net
|
||||
periodicEvent = new AsyncResetEvent();
|
||||
periodicTask = Task.Run(async () =>
|
||||
{
|
||||
while (!disposing)
|
||||
while (!_disposing)
|
||||
{
|
||||
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
|
||||
if (disposing)
|
||||
if (_disposing)
|
||||
break;
|
||||
|
||||
foreach (var socket in sockets.Values)
|
||||
|
||||
foreach (var socketConnection in socketConnections.Values)
|
||||
{
|
||||
if (disposing)
|
||||
if (_disposing)
|
||||
break;
|
||||
|
||||
if (!socket.Socket.IsOpen)
|
||||
if (!socketConnection.Connected)
|
||||
continue;
|
||||
|
||||
var obj = objGetter(socket);
|
||||
var obj = objGetter(socketConnection);
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
log.Write(LogLevel.Trace, $"Socket {socket.Socket.Id} sending periodic");
|
||||
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
|
||||
|
||||
try
|
||||
{
|
||||
socket.Send(obj);
|
||||
socketConnection.Send(obj);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"Socket {socket.Socket.Id} Periodic send failed: " + ex);
|
||||
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -612,12 +720,11 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
public virtual async Task<bool> UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
|
||||
SocketSubscription? subscription = null;
|
||||
SocketConnection? connection = null;
|
||||
foreach(var socket in sockets.Values.ToList())
|
||||
foreach (var socket in socketConnections.Values.ToList())
|
||||
{
|
||||
subscription = socket.GetSubscription(subscriptionId);
|
||||
if (subscription != null)
|
||||
@@ -628,10 +735,11 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
if (subscription == null || connection == null)
|
||||
return;
|
||||
return false;
|
||||
|
||||
log.Write(LogLevel.Information, "Closing subscription " + subscriptionId);
|
||||
_logger.Log(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
|
||||
await connection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -644,7 +752,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
log.Write(LogLevel.Information, "Closing subscription " + subscription.Id);
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -654,19 +762,48 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||
|
||||
await Task.Run(async () =>
|
||||
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = sockets.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.CloseAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {socketConnections.Count} connections");
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var sub in socketList)
|
||||
tasks.Add(sub.TriggerReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
|
||||
foreach (var connection in socketConnections)
|
||||
{
|
||||
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
|
||||
foreach (var subscription in connection.Value.Subscriptions)
|
||||
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -674,11 +811,14 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
disposing = true;
|
||||
_disposing = true;
|
||||
periodicEvent?.Set();
|
||||
periodicEvent?.Dispose();
|
||||
log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
|
||||
{
|
||||
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
|
||||
_ = UnsubscribeAllAsync();
|
||||
}
|
||||
semaphoreSlim?.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Balance data
|
||||
/// </summary>
|
||||
public class Balance: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset name
|
||||
/// </summary>
|
||||
public string Asset { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Quantity available
|
||||
/// </summary>
|
||||
public decimal? Available { get; set; }
|
||||
/// <summary>
|
||||
/// Total quantity
|
||||
/// </summary>
|
||||
public decimal? Total { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for common objects
|
||||
/// </summary>
|
||||
public class BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The source object the data is derived from
|
||||
/// </summary>
|
||||
public object SourceObject { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order type
|
||||
/// </summary>
|
||||
public enum CommonOrderType
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit type
|
||||
/// </summary>
|
||||
Limit,
|
||||
/// <summary>
|
||||
/// Market type
|
||||
/// </summary>
|
||||
Market,
|
||||
/// <summary>
|
||||
/// Other order type
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Order side
|
||||
/// </summary>
|
||||
public enum CommonOrderSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Buy order
|
||||
/// </summary>
|
||||
Buy,
|
||||
/// <summary>
|
||||
/// Sell order
|
||||
/// </summary>
|
||||
Sell
|
||||
}
|
||||
/// <summary>
|
||||
/// Order status
|
||||
/// </summary>
|
||||
public enum CommonOrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// placed and not fully filled order
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// canceled order
|
||||
/// </summary>
|
||||
Canceled,
|
||||
/// <summary>
|
||||
/// filled order
|
||||
/// </summary>
|
||||
Filled
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public enum CommonPositionSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Long position
|
||||
/// </summary>
|
||||
Long,
|
||||
/// <summary>
|
||||
/// Short position
|
||||
/// </summary>
|
||||
Short,
|
||||
/// <summary>
|
||||
/// Both
|
||||
/// </summary>
|
||||
Both
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Kline data
|
||||
/// </summary>
|
||||
public class Kline: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Opening time of the kline
|
||||
/// </summary>
|
||||
public DateTime OpenTime { get; set; }
|
||||
/// <summary>
|
||||
/// Price at the open time
|
||||
/// </summary>
|
||||
public decimal? OpenPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Highest price of the kline
|
||||
/// </summary>
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Lowest price of the kline
|
||||
/// </summary>
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Close price of the kline
|
||||
/// </summary>
|
||||
public decimal? ClosePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the kline
|
||||
/// </summary>
|
||||
public decimal? Volume { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order data
|
||||
/// </summary>
|
||||
public class Order: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the order
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Symbol of the order
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
public decimal? Price { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// The quantity of the order which has been filled
|
||||
/// </summary>
|
||||
public decimal? QuantityFilled { get; set; }
|
||||
/// <summary>
|
||||
/// Status of the order
|
||||
/// </summary>
|
||||
public CommonOrderStatus Status { get; set; }
|
||||
/// <summary>
|
||||
/// Side of the order
|
||||
/// </summary>
|
||||
public CommonOrderSide Side { get; set; }
|
||||
/// <summary>
|
||||
/// Type of the order
|
||||
/// </summary>
|
||||
public CommonOrderType Type { get; set; }
|
||||
/// <summary>
|
||||
/// Order time
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book data
|
||||
/// </summary>
|
||||
public class OrderBook: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// List of bids
|
||||
/// </summary>
|
||||
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
|
||||
/// <summary>
|
||||
/// List of asks
|
||||
/// </summary>
|
||||
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book entry
|
||||
/// </summary>
|
||||
public class OrderBookEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Quantity of the entry
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Price of the entry
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of an order
|
||||
/// </summary>
|
||||
public class OrderId: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of an order
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Position data
|
||||
/// </summary>
|
||||
public class Position: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the position
|
||||
/// </summary>
|
||||
public string? Id { get; set; }
|
||||
/// <summary>
|
||||
/// Symbol of the position
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Leverage
|
||||
/// </summary>
|
||||
public decimal Leverage { get; set; }
|
||||
/// <summary>
|
||||
/// Position quantity
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Entry price
|
||||
/// </summary>
|
||||
public decimal? EntryPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Liquidation price
|
||||
/// </summary>
|
||||
public decimal? LiquidationPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Unrealized profit and loss
|
||||
/// </summary>
|
||||
public decimal? UnrealizedPnl { get; set; }
|
||||
/// <summary>
|
||||
/// Realized profit and loss
|
||||
/// </summary>
|
||||
public decimal? RealizedPnl { get; set; }
|
||||
/// <summary>
|
||||
/// Mark price
|
||||
/// </summary>
|
||||
public decimal? MarkPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Auto adding margin
|
||||
/// </summary>
|
||||
public bool? AutoMargin { get; set; }
|
||||
/// <summary>
|
||||
/// Position margin
|
||||
/// </summary>
|
||||
public decimal? PositionMargin { get; set; }
|
||||
/// <summary>
|
||||
/// Position side
|
||||
/// </summary>
|
||||
public CommonPositionSide? Side { get; set; }
|
||||
/// <summary>
|
||||
/// Is isolated
|
||||
/// </summary>
|
||||
public bool? Isolated { get; set; }
|
||||
/// <summary>
|
||||
/// Maintenance margin
|
||||
/// </summary>
|
||||
public decimal? MaintananceMargin { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol data
|
||||
/// </summary>
|
||||
public class Symbol: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the symbol
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Minimal quantity of an order
|
||||
/// </summary>
|
||||
public decimal? MinTradeQuantity { get; set; }
|
||||
/// <summary>
|
||||
/// Step with which the quantity should increase
|
||||
/// </summary>
|
||||
public decimal? QuantityStep { get; set; }
|
||||
/// <summary>
|
||||
/// step with which the price should increase
|
||||
/// </summary>
|
||||
public decimal? PriceStep { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of decimals for quantity
|
||||
/// </summary>
|
||||
public int? QuantityDecimals { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of decimal for price
|
||||
/// </summary>
|
||||
public int? PriceDecimals { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticker data
|
||||
/// </summary>
|
||||
public class Ticker: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Price 24 hours ago
|
||||
/// </summary>
|
||||
public decimal? Price24H { get; set; }
|
||||
/// <summary>
|
||||
/// Last trade price
|
||||
/// </summary>
|
||||
public decimal? LastPrice { get; set; }
|
||||
/// <summary>
|
||||
/// 24 hour low price
|
||||
/// </summary>
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// 24 hour high price
|
||||
/// </summary>
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// 24 hour volume
|
||||
/// </summary>
|
||||
public decimal? Volume { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade data
|
||||
/// </summary>
|
||||
public class Trade: BaseCommonObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol of the trade
|
||||
/// </summary>
|
||||
public string Symbol { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
public decimal Price { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp of the trade
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User trade info
|
||||
/// </summary>
|
||||
public class UserTrade: Trade
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the trade
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Order id of the trade
|
||||
/// </summary>
|
||||
public string? OrderId { get; set; }
|
||||
/// <summary>
|
||||
/// Fee of the trade
|
||||
/// </summary>
|
||||
public decimal? Fee { get; set; }
|
||||
/// <summary>
|
||||
/// The asset the fee is paid in
|
||||
/// </summary>
|
||||
public string? FeeAsset { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </summary>
|
||||
public class ArrayConverter : JsonConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
@@ -28,6 +28,9 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
if (objectType == typeof(JToken))
|
||||
return JToken.Load(reader);
|
||||
|
||||
@@ -36,7 +39,7 @@ namespace CryptoExchange.Net.Converters
|
||||
return ParseObject(arr, result, objectType);
|
||||
}
|
||||
|
||||
private static object? ParseObject(JArray arr, object result, Type objectType)
|
||||
private static object ParseObject(JArray arr, object result, Type objectType)
|
||||
{
|
||||
foreach (var property in objectType.GetProperties())
|
||||
{
|
||||
@@ -63,8 +66,8 @@ namespace CryptoExchange.Net.Converters
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
|
||||
foreach (var obj in innerArray)
|
||||
{
|
||||
var innerObj = Activator.CreateInstance(objType);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType);
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
|
||||
count++;
|
||||
}
|
||||
property.SetValue(result, arrayResult);
|
||||
@@ -72,8 +75,8 @@ namespace CryptoExchange.Net.Converters
|
||||
else
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||
var innerObj = Activator.CreateInstance(objType);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType);
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
continue;
|
||||
@@ -97,16 +100,20 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
if (value != null && property.PropertyType.IsInstanceOfType(value))
|
||||
{
|
||||
property.SetValue(result, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (value is JToken token)
|
||||
{
|
||||
if (token.Type == JTokenType.Null)
|
||||
value = null;
|
||||
}
|
||||
|
||||
if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().Contains("e")))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
property.SetValue(result, dec);
|
||||
@@ -172,15 +179,16 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||
(T?)attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
|
||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||
(T?)attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark property as an index in the array
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ArrayPropertyAttribute: Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// The enum->string mapping
|
||||
/// </summary>
|
||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
||||
private readonly bool quotes;
|
||||
private readonly bool _quotes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <param name="useQuotes"></param>
|
||||
protected BaseConverter(bool useQuotes)
|
||||
{
|
||||
quotes = useQuotes;
|
||||
_quotes = useQuotes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = value == null? null: GetValue((T) value);
|
||||
if (quotes)
|
||||
if (_quotes)
|
||||
writer.WriteValue(stringValue);
|
||||
else
|
||||
writer.WriteRawValue(stringValue);
|
||||
@@ -43,9 +43,13 @@ namespace CryptoExchange.Net.Converters
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if (!GetValue(reader.Value.ToString(), out var result))
|
||||
var stringValue = reader.Value.ToString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (!GetValue(stringValue, out var result))
|
||||
{
|
||||
Debug.WriteLine($"Cannot map enum. Type: {typeof(T)}, Value: {reader.Value}");
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +75,7 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
private bool GetValue(string value, out T result)
|
||||
{
|
||||
//check for exact match first, then if not found fallback to a case insensitive match
|
||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
||||
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if(mapping.Equals(default(KeyValuePair<T, string>)))
|
||||
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
||||
/// </summary>
|
||||
public class DateTimeConverter: JsonConverter
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if(reader.TokenType is JsonToken.Integer)
|
||||
{
|
||||
var longValue = (long)reader.Value;
|
||||
if (longValue == 0 || longValue == -1)
|
||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||
if (longValue < 19999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonToken.Float)
|
||||
{
|
||||
var doubleValue = (double)reader.Value;
|
||||
if (doubleValue == 0 || doubleValue == -1)
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
|
||||
if (doubleValue < 19999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
|
||||
return ConvertFromMilliseconds(doubleValue);
|
||||
}
|
||||
else if(reader.TokenType is JsonToken.String)
|
||||
{
|
||||
var stringValue = (string)reader.Value;
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
// Parse 20211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 6)
|
||||
{
|
||||
// Parse 211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (doubleValue < 19999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
if (doubleValue < 19999999999999)
|
||||
return ConvertFromMilliseconds((long)doubleValue);
|
||||
if (doubleValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds((long)doubleValue);
|
||||
|
||||
return ConvertFromNanoseconds((long)doubleValue);
|
||||
}
|
||||
|
||||
if(stringValue.Length == 10)
|
||||
{
|
||||
// Parse 2021-11-03 format
|
||||
var values = stringValue.Split('-');
|
||||
if(!int.TryParse(values[0], out var year)
|
||||
|| !int.TryParse(values[1], out var month)
|
||||
|| !int.TryParse(values[2], out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
else if(reader.TokenType == JsonToken.Date)
|
||||
{
|
||||
return (DateTime)reader.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="milliseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="microseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="nanoseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null: (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var datetimeValue = (DateTime?)value;
|
||||
if (datetimeValue == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
if(datetimeValue == default(DateTime))
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||
/// </summary>
|
||||
public class EnumConverter : JsonConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsEnum;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
if (!_mapping.TryGetValue(enumType, out var mapping))
|
||||
mapping = AddMapping(enumType);
|
||||
|
||||
var stringValue = reader.Value?.ToString();
|
||||
if (stringValue == null)
|
||||
{
|
||||
// Received null value
|
||||
var emptyResult = GetDefaultValue(objectType, enumType);
|
||||
if(emptyResult != null)
|
||||
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
||||
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
if (!GetValue(enumType, mapping, stringValue!, out var result))
|
||||
{
|
||||
var defaultValue = GetDefaultValue(objectType, enumType);
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
if (defaultValue != null)
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
// We received an enum value but weren't able to parse it.
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return null;
|
||||
|
||||
return Activator.CreateInstance(enumType); // return default value
|
||||
}
|
||||
|
||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
||||
{
|
||||
var mapping = new List<KeyValuePair<object, string>>();
|
||||
var enumMembers = objectType.GetMembers();
|
||||
foreach (var member in enumMembers)
|
||||
{
|
||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||
foreach (MapAttribute attribute in maps)
|
||||
{
|
||||
foreach (var value in attribute.Values)
|
||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
||||
}
|
||||
}
|
||||
_mapping.TryAdd(objectType, mapping);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
||||
{
|
||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
{
|
||||
result = mapping.Key;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
result = Enum.Parse(objectType, value, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString<T>(T enumValue)
|
||||
{
|
||||
var objectType = typeof(T);
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = GetString(value);
|
||||
writer.WriteValue(stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// converter for milliseconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var t = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if(value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for nanoseconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampNanoSecondsConverter : JsonConverter
|
||||
{
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var nanoSeconds = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerNanosecond));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).Ticks / ticksPerNanosecond));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for seconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampSecondsConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if (reader.Value is double d)
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d);
|
||||
|
||||
var t = double.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture);
|
||||
// Set ticks instead of seconds or milliseconds, because AddSeconds/AddMilliseconds rounds to nearest millisecond
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)(t * TimeSpan.TicksPerSecond));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// converter for datetime string (yyyymmdd) to datetime
|
||||
/// </summary>
|
||||
public class TimestampStringConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var value = reader.Value.ToString();
|
||||
if (value.Length == 8)
|
||||
return new DateTime(int.Parse(value.Substring(0, 4)), int.Parse(value.Substring(4, 2)), int.Parse(value.Substring(6, 2)), 0, 0, 0, DateTimeKind.Utc);
|
||||
else if(value.Length == 6)
|
||||
return new DateTime(int.Parse(value.Substring(0, 2)), int.Parse(value.Substring(2, 2)), int.Parse(value.Substring(4, 2)), 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
throw new Exception("Unknown datetime value: " + value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
{
|
||||
var dateTimeValue = (DateTime)value;
|
||||
writer.WriteValue(int.Parse($"{dateTimeValue.Year}{dateTimeValue.Month}{dateTimeValue.Day}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for utc datetime
|
||||
/// </summary>
|
||||
public class UTCDateTimeConverter: JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(JsonConvert.SerializeObject(value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
DateTime value;
|
||||
if (reader.Value is string s)
|
||||
value = (DateTime)JsonConvert.DeserializeObject(s)!;
|
||||
else
|
||||
value = (DateTime) reader.Value;
|
||||
|
||||
return DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,19 @@
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency exchange API's</Description>
|
||||
<PackageVersion>4.2.8</PackageVersion>
|
||||
<AssemblyVersion>4.2.8</AssemblyVersion>
|
||||
<FileVersion>4.2.8</FileVersion>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>6.0.3</PackageVersion>
|
||||
<AssemblyVersion>6.0.3</AssemblyVersion>
|
||||
<FileVersion>6.0.3</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>4.2.8 - Fixed deadlock in socket receive, Fixed issue in reconnection handling when the client is disconnected again during resubscribing, Added some additional checking of socket state to prevent sending/expecting data when socket is not connected</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>6.0.3 - Fixed Proxy not getting applied in rest clients when not using DI</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
@@ -41,11 +41,15 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,9 @@ namespace CryptoExchange.Net
|
||||
|
||||
var offset = value % step.Value;
|
||||
if(roundingType == RoundingType.Down)
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common balance
|
||||
/// </summary>
|
||||
public interface ICommonBalance
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset name
|
||||
/// </summary>
|
||||
public string CommonAsset { get; }
|
||||
/// <summary>
|
||||
/// Amount available
|
||||
/// </summary>
|
||||
public decimal CommonAvailable { get; }
|
||||
/// <summary>
|
||||
/// Total amount
|
||||
/// </summary>
|
||||
public decimal CommonTotal { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common trade
|
||||
/// </summary>
|
||||
public interface ICommonTrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the trade
|
||||
/// </summary>
|
||||
public string CommonId { get; }
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
public decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
public decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Fee paid for the trade
|
||||
/// </summary>
|
||||
public decimal CommonFee { get; }
|
||||
/// <summary>
|
||||
/// The asset fee was paid in
|
||||
/// </summary>
|
||||
public string? CommonFeeAsset { get; }
|
||||
/// <summary>
|
||||
/// Trade time
|
||||
/// </summary>
|
||||
DateTime CommonTradeTime { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common kline
|
||||
/// </summary>
|
||||
public interface ICommonKline
|
||||
{
|
||||
/// <summary>
|
||||
/// High price for this kline
|
||||
/// </summary>
|
||||
decimal CommonHigh { get; }
|
||||
/// <summary>
|
||||
/// Low price for this kline
|
||||
/// </summary>
|
||||
decimal CommonLow { get; }
|
||||
/// <summary>
|
||||
/// Open price for this kline
|
||||
/// </summary>
|
||||
decimal CommonOpen { get; }
|
||||
/// <summary>
|
||||
/// Close price for this kline
|
||||
/// </summary>
|
||||
decimal CommonClose { get; }
|
||||
/// <summary>
|
||||
/// Open time for this kline
|
||||
/// </summary>
|
||||
DateTime CommonOpenTime { get; }
|
||||
/// <summary>
|
||||
/// Volume of this kline
|
||||
/// </summary>
|
||||
decimal CommonVolume { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common order
|
||||
/// </summary>
|
||||
public interface ICommonOrder: ICommonOrderId
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol of the order
|
||||
/// </summary>
|
||||
public string CommonSymbol { get; }
|
||||
/// <summary>
|
||||
/// Price of the order
|
||||
/// </summary>
|
||||
public decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the order
|
||||
/// </summary>
|
||||
public decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Status of the order
|
||||
/// </summary>
|
||||
public IExchangeClient.OrderStatus CommonStatus { get; }
|
||||
/// <summary>
|
||||
/// Whether the order is active
|
||||
/// </summary>
|
||||
public bool IsActive { get; }
|
||||
/// <summary>
|
||||
/// Side of the order
|
||||
/// </summary>
|
||||
public IExchangeClient.OrderSide CommonSide { get; }
|
||||
/// <summary>
|
||||
/// Type of the order
|
||||
/// </summary>
|
||||
public IExchangeClient.OrderType CommonType { get; }
|
||||
/// <summary>
|
||||
/// order time
|
||||
/// </summary>
|
||||
DateTime CommonOrderTime { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common order book
|
||||
/// </summary>
|
||||
public interface ICommonOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Bids
|
||||
/// </summary>
|
||||
IEnumerable<ISymbolOrderBookEntry> CommonBids { get; }
|
||||
/// <summary>
|
||||
/// Asks
|
||||
/// </summary>
|
||||
IEnumerable<ISymbolOrderBookEntry> CommonAsks { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common order id
|
||||
/// </summary>
|
||||
public interface ICommonOrderId
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the order
|
||||
/// </summary>
|
||||
public string CommonId { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Recent trade
|
||||
/// </summary>
|
||||
public interface ICommonRecentTrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Trade time
|
||||
/// </summary>
|
||||
DateTime CommonTradeTime { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common symbol
|
||||
/// </summary>
|
||||
public interface ICommonSymbol
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
public string CommonName { get; }
|
||||
/// <summary>
|
||||
/// Minimum trade size
|
||||
/// </summary>
|
||||
public decimal CommonMinimumTradeSize { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common ticker
|
||||
/// </summary>
|
||||
public interface ICommonTicker
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
public string CommonSymbol { get; }
|
||||
/// <summary>
|
||||
/// High price
|
||||
/// </summary>
|
||||
public decimal CommonHigh { get; }
|
||||
/// <summary>
|
||||
/// Low price
|
||||
/// </summary>
|
||||
public decimal CommonLow { get; }
|
||||
/// <summary>
|
||||
/// Volume
|
||||
/// </summary>
|
||||
public decimal CommonVolume { get; }
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
@@ -74,7 +72,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
||||
{
|
||||
if(value != null)
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
@@ -91,31 +89,6 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="converter"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, string> parameters, string key, string? value, JsonConverter converter)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a query string of the specified parameters
|
||||
/// </summary>
|
||||
@@ -129,8 +102,10 @@ namespace CryptoExchange.Net
|
||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||
foreach (var arrayEntry in arraysParameters)
|
||||
{
|
||||
if(serializationType == ArrayParametersSerialization.Array)
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
}
|
||||
else
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
@@ -144,6 +119,31 @@ namespace CryptoExchange.Net
|
||||
return uriString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary to formdata string
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFormData(this SortedDictionary<string, object> parameters)
|
||||
{
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
foreach (var value in array)
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||
}
|
||||
}
|
||||
return formData.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the string the secure string is representing
|
||||
/// </summary>
|
||||
@@ -177,6 +177,45 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Are 2 secure strings equal
|
||||
/// </summary>
|
||||
/// <param name="ss1">Source secure string</param>
|
||||
/// <param name="ss2">Compare secure string</param>
|
||||
/// <returns>True if equal by value</returns>
|
||||
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
|
||||
{
|
||||
IntPtr bstr1 = IntPtr.Zero;
|
||||
IntPtr bstr2 = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
bstr1 = Marshal.SecureStringToBSTR(ss1);
|
||||
bstr2 = Marshal.SecureStringToBSTR(ss2);
|
||||
int length1 = Marshal.ReadInt32(bstr1, -4);
|
||||
int length2 = Marshal.ReadInt32(bstr2, -4);
|
||||
if (length1 == length2)
|
||||
{
|
||||
for (int x = 0; x < length1; ++x)
|
||||
{
|
||||
byte b1 = Marshal.ReadByte(bstr1, x);
|
||||
byte b2 = Marshal.ReadByte(bstr2, x);
|
||||
if (b1 != b2) return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
|
||||
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a secure string from a string
|
||||
/// </summary>
|
||||
@@ -195,9 +234,9 @@ namespace CryptoExchange.Net
|
||||
/// String to JToken
|
||||
/// </summary>
|
||||
/// <param name="stringData"></param>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static JToken? ToJToken(this string stringData, Log? log = null)
|
||||
public static JToken? ToJToken(this string stringData, ILogger? logger = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stringData))
|
||||
return null;
|
||||
@@ -209,15 +248,15 @@ namespace CryptoExchange.Net
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Debug.WriteLine(info);
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
|
||||
log?.Write(LogLevel.Error, info);
|
||||
if (log == null) Debug.WriteLine(info);
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -231,8 +270,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||
{
|
||||
if (!allowedValues.Contains(value))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -245,8 +286,10 @@ namespace CryptoExchange.Net
|
||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||
{
|
||||
if (value < minValue || value > maxValue)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -298,7 +341,7 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToLogString(this Exception exception)
|
||||
public static string ToLogString(this Exception? exception)
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
var indent = 0;
|
||||
@@ -319,6 +362,125 @@ namespace CryptoExchange.Net
|
||||
|
||||
return message.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append a base url with provided path
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string AppendPath(this string url, params string[] path)
|
||||
{
|
||||
if (!url.EndsWith("/"))
|
||||
url += "/";
|
||||
|
||||
foreach (var item in path)
|
||||
url += item.Trim('/') + "/";
|
||||
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
public static string FillPathParameters(this string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Port = baseUri.Port;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if(parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
var uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
uriBuilder.Host = baseUri.Host;
|
||||
uriBuilder.Port = baseUri.Port;
|
||||
uriBuilder.Path = baseUri.AbsolutePath;
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add parameter to URI
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||
{
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
httpValueCollection.Remove(name);
|
||||
httpValueCollection.Add(name, value);
|
||||
|
||||
var ub = new UriBuilder(uri);
|
||||
ub.Query = httpValueCollection.ToString();
|
||||
|
||||
return ub.Uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+73
-112
@@ -1,50 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared interface for exchange wrappers based on the CryptoExchange.Net package
|
||||
/// Common rest client endpoints
|
||||
/// </summary>
|
||||
public interface IExchangeClient
|
||||
public interface IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the exchange
|
||||
/// </summary>
|
||||
string ExchangeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Should be triggered on order placing
|
||||
/// </summary>
|
||||
event Action<ICommonOrderId> OnOrderPlaced;
|
||||
event Action<OrderId> OnOrderPlaced;
|
||||
/// <summary>
|
||||
/// Should be triggered on order cancelling
|
||||
/// </summary>
|
||||
event Action<ICommonOrderId> OnOrderCanceled;
|
||||
event Action<OrderId> OnOrderCanceled;
|
||||
|
||||
/// <summary>
|
||||
/// Get the symbol name based on a base and quote asset
|
||||
/// </summary>
|
||||
/// <param name="baseAsset"></param>
|
||||
/// <param name="quoteAsset"></param>
|
||||
/// <param name="baseAsset">The base asset</param>
|
||||
/// <param name="quoteAsset">The quote asset</param>
|
||||
/// <returns></returns>
|
||||
string GetSymbolName(string baseAsset, string quoteAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of symbols for the exchange
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonSymbol>>> GetSymbolsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of tickers for the exchange
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonTicker>>> GetTickersAsync();
|
||||
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a ticker for the exchange
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get klines for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonTicker>> GetTickerAsync(string symbol);
|
||||
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of tickers for the exchange
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of candles for a given symbol on the exchange
|
||||
@@ -54,124 +64,75 @@ namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
|
||||
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
|
||||
/// <param name="limit">[Optional] Max number of results</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonKline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
|
||||
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the order book for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the book for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrderBook>> GetOrderBookAsync(string symbol);
|
||||
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// The recent trades for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the trades for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonRecentTrade>>> GetRecentTradesAsync(string symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<ICommonOrderId>> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal quantity, decimal? price = null, string? accountId = null);
|
||||
/// <summary>
|
||||
/// Get an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrder>> GetOrderAsync(string orderId, string? symbol = null);
|
||||
/// <summary>
|
||||
/// Get trades for an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonTrade>>> GetTradesAsync(string orderId, string? symbol = null);
|
||||
/// <summary>
|
||||
/// Get a list of open orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetOpenOrdersAsync(string? symbol = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of closed orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetClosedOrdersAsync(string? symbol = null);
|
||||
/// <summary>
|
||||
/// Cancel an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrderId>> CancelOrderAsync(string orderId, string? symbol = null);
|
||||
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get balances
|
||||
/// </summary>
|
||||
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonBalance>>> GetBalancesAsync(string? accountId = null);
|
||||
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Common order id
|
||||
/// Get an order by id
|
||||
/// </summary>
|
||||
public enum OrderType
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit type
|
||||
/// </summary>
|
||||
Limit,
|
||||
/// <summary>
|
||||
/// Market type
|
||||
/// </summary>
|
||||
Market,
|
||||
/// <summary>
|
||||
/// Other order type
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Common order side
|
||||
/// Get trades for an order by id
|
||||
/// </summary>
|
||||
public enum OrderSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Buy order
|
||||
/// </summary>
|
||||
Buy,
|
||||
/// <summary>
|
||||
/// Sell order
|
||||
/// </summary>
|
||||
Sell
|
||||
}
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Common order status
|
||||
/// Get a list of open orders
|
||||
/// </summary>
|
||||
public enum OrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// placed and not fully filled order
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// cancelled order
|
||||
/// </summary>
|
||||
Canceled,
|
||||
/// <summary>
|
||||
/// filled order
|
||||
/// </summary>
|
||||
Filled
|
||||
}
|
||||
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of closed orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common futures endpoints
|
||||
/// </summary>
|
||||
public interface IFuturesClient : IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="leverage">[Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand)</param>
|
||||
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get position
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common spot endpoints
|
||||
/// </summary>
|
||||
public interface ISpotClient: IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Base api client
|
||||
/// </summary>
|
||||
public interface IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Base address
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this API client
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="credentials"></param>
|
||||
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -8,13 +13,17 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface IRateLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit the request if needed
|
||||
/// Limit a request based on previous requests made
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="limitBehaviour"></param>
|
||||
/// <param name="credits"></param>
|
||||
/// <returns></returns>
|
||||
CallResult<double> LimitRequest(RestClient client, string url, RateLimitingBehaviour limitBehaviour, int credits=1);
|
||||
/// <param name="log">The logger</param>
|
||||
/// <param name="endpoint">The endpoint the request is for</param>
|
||||
/// <param name="method">The Http request method</param>
|
||||
/// <param name="signed">Whether the request is singed(private) or not</param>
|
||||
/// <param name="apiKey">The api key making this request</param>
|
||||
/// <param name="limitBehaviour">The limit behavior for when the limit is reached</param>
|
||||
/// <param name="requestWeight">The weight of the request</param>
|
||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||
/// <returns>The time in milliseconds spend waiting</returns>
|
||||
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -16,14 +17,14 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="requestId"></param>
|
||||
/// <returns></returns>
|
||||
IRequest Create(HttpMethod method, string uri, int requestId);
|
||||
IRequest Create(HttpMethod method, Uri uri, int requestId);
|
||||
|
||||
/// <summary>
|
||||
/// Configure the requests created by this factory
|
||||
/// </summary>
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
/// <param name="proxy">Proxy settings to use</param>
|
||||
/// <param name="httpClient">Optional shared http client instance</param>
|
||||
void Configure(TimeSpan requestTimeout, ApiProxy? proxy, HttpClient? httpClient=null);
|
||||
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
bool IsSuccessStatusCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the response in bytes
|
||||
/// </summary>
|
||||
long? ContentLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client
|
||||
/// </summary>
|
||||
public interface IRestApiClient : IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating requests. Used for unit testing
|
||||
/// </summary>
|
||||
IRequestFactory RequestFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <summary>
|
||||
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
TimeSyncInfo? GetTimeSyncInfo();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -13,56 +11,13 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface IRestClient: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating requests. Used for unit testing
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
IRequestFactory RequestFactory { get; set; }
|
||||
ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// What should happen when hitting a rate limit
|
||||
/// </summary>
|
||||
RateLimitingBehaviour RateLimitBehaviour { get; }
|
||||
|
||||
/// <summary>
|
||||
/// List of active rate limiters
|
||||
/// </summary>
|
||||
IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of requests made
|
||||
/// The total amount of requests made with this client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the API
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// </summary>
|
||||
string ExchangeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rate limiter to the client. There are 2 choices, the <see cref="RateLimiterTotal"/> and the <see cref="RateLimiterPerEndpoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="limiter">The limiter to add</param>
|
||||
void AddRateLimiter(IRateLimiter limiter);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all rate limiters from this client
|
||||
/// </summary>
|
||||
void RemoveRateLimiters();
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
CallResult<long> Ping(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ping to see if the server is reachable
|
||||
/// </summary>
|
||||
/// <returns>The roundtrip time of the ping request</returns>
|
||||
Task<CallResult<long>> PingAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket API client
|
||||
/// </summary>
|
||||
public interface ISocketApiClient: IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The current amount of socket connections on the API client
|
||||
/// </summary>
|
||||
int CurrentConnections { get; }
|
||||
/// <summary>
|
||||
/// The current amount of subscriptions over all connections
|
||||
/// </summary>
|
||||
int CurrentSubscriptions { get; }
|
||||
/// <summary>
|
||||
/// Incoming data kpbs
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to reconnect to after losing a connection
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
string GetSubscriptionsState();
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ReconnectAsync();
|
||||
/// <summary>
|
||||
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||
/// </summary>
|
||||
/// <param name="request">The original request</param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<object>> RevitalizeRequestAsync(object request);
|
||||
/// <summary>
|
||||
/// Periodically sends data over a socket connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">Identifier for the periodic send</param>
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAllAsync();
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UnsubscribeAsync(int subscriptionId);
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@@ -11,48 +13,31 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface ISocketClient: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time in between reconnect attempts
|
||||
/// Incoming kilobytes per second of data
|
||||
/// </summary>
|
||||
TimeSpan ReconnectInterval { get; }
|
||||
public double IncomingKbps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
|
||||
/// </summary>
|
||||
public int CurrentConnections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the client should try to auto reconnect when losing connection
|
||||
/// The current amount of subscriptions running from the client
|
||||
/// </summary>
|
||||
bool AutoReconnect { get; }
|
||||
public int CurrentSubscriptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the API
|
||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketResponseTimeout"/>
|
||||
TimeSpan ResponseTimeout { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketNoDataTimeout"/>
|
||||
TimeSpan SocketNoDataTimeout { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of concurrent socket connections
|
||||
/// </summary>
|
||||
int MaxSocketConnections { get; }
|
||||
|
||||
/// <inheritdoc cref="SocketClientOptions.SocketSubscriptionsCombineTarget"/>
|
||||
int SocketCombineTarget { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxReconnectTries"/>
|
||||
int? MaxReconnectTries { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxResubscribeTries"/>
|
||||
int? MaxResubscribeTries { get; }
|
||||
/// <inheritdoc cref="SocketClientOptions.MaxConcurrentResubscriptionsPerSocket"/>
|
||||
int MaxConcurrentResubscriptionsPerSocket { get; }
|
||||
/// <summary>
|
||||
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAsync(int subscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from a stream
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
@@ -10,6 +11,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
public interface ISymbolOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The status of the order book. Order book is up to date when the status is `Synced`
|
||||
/// </summary>
|
||||
@@ -39,7 +45,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Timestamp of the last update
|
||||
/// </summary>
|
||||
DateTime LastOrderBookUpdate { get; }
|
||||
DateTime UpdateTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of asks in the book
|
||||
@@ -83,8 +89,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Start connecting and synchronizing the order book
|
||||
/// </summary>
|
||||
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<bool>> StartAsync();
|
||||
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
|
||||
|
||||
/// <summary>
|
||||
/// Stop syncing the order book
|
||||
@@ -94,13 +101,22 @@ namespace CryptoExchange.Net.Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
|
||||
/// at that price since between this calculation and the order placement the book can have changed.
|
||||
/// at that price since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quantity">The quantity in base asset to fill</param>
|
||||
/// <param name="type">The type</param>
|
||||
/// <returns>Average fill price</returns>
|
||||
CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type);
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
|
||||
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
|
||||
/// <param name="type">The type</param>
|
||||
/// <returns>Amount of base asset tradable with the specified amount of quote asset</returns>
|
||||
CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type);
|
||||
|
||||
/// <summary>
|
||||
/// String representation of the top x entries
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
@@ -7,80 +8,60 @@ using System.Threading.Tasks;
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for websocket interaction
|
||||
/// Webscoket connection interface
|
||||
/// </summary>
|
||||
public interface IWebsocket: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Websocket closed
|
||||
/// Websocket closed event
|
||||
/// </summary>
|
||||
event Action OnClose;
|
||||
/// <summary>
|
||||
/// Websocket message received
|
||||
/// Websocket message received event
|
||||
/// </summary>
|
||||
event Action<string> OnMessage;
|
||||
/// <summary>
|
||||
/// Websocket error
|
||||
/// Websocket error event
|
||||
/// </summary>
|
||||
event Action<Exception> OnError;
|
||||
/// <summary>
|
||||
/// Websocket opened
|
||||
/// Websocket opened event
|
||||
/// </summary>
|
||||
event Action OnOpen;
|
||||
/// <summary>
|
||||
/// Websocket has lost connection to the server and is attempting to reconnect
|
||||
/// </summary>
|
||||
event Action OnReconnecting;
|
||||
/// <summary>
|
||||
/// Websocket has reconnected to the server
|
||||
/// </summary>
|
||||
event Action OnReconnected;
|
||||
/// <summary>
|
||||
/// Get reconntion url
|
||||
/// </summary>
|
||||
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id
|
||||
/// Unique id for this socket
|
||||
/// </summary>
|
||||
int Id { get; }
|
||||
/// <summary>
|
||||
/// Origin
|
||||
/// </summary>
|
||||
string? Origin { get; set; }
|
||||
/// <summary>
|
||||
/// Encoding to use
|
||||
/// </summary>
|
||||
Encoding? Encoding { get; set; }
|
||||
/// <summary>
|
||||
/// Reconnecting
|
||||
/// </summary>
|
||||
bool Reconnecting { get; set; }
|
||||
/// <summary>
|
||||
/// The max amount of outgoing messages per second
|
||||
/// </summary>
|
||||
int? RatelimitPerSecond { get; set; }
|
||||
/// <summary>
|
||||
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <summary>
|
||||
/// Handler for byte data
|
||||
/// The uri the socket connects to
|
||||
/// </summary>
|
||||
Func<byte[], string>? DataInterpreterBytes { get; set; }
|
||||
Uri Uri { get; }
|
||||
/// <summary>
|
||||
/// Handler for string data
|
||||
/// </summary>
|
||||
Func<string, string>? DataInterpreterString { get; set; }
|
||||
/// <summary>
|
||||
/// Socket url
|
||||
/// </summary>
|
||||
string Url { get; }
|
||||
/// <summary>
|
||||
/// Is closed
|
||||
/// Whether the socket connection is closed
|
||||
/// </summary>
|
||||
bool IsClosed { get; }
|
||||
/// <summary>
|
||||
/// Is open
|
||||
/// Whether the socket connection is open
|
||||
/// </summary>
|
||||
bool IsOpen { get; }
|
||||
/// <summary>
|
||||
/// Supported ssl protocols
|
||||
/// </summary>
|
||||
SslProtocols SSLProtocols { get; set; }
|
||||
/// <summary>
|
||||
/// Timeout
|
||||
/// </summary>
|
||||
TimeSpan Timeout { get; set; }
|
||||
/// <summary>
|
||||
/// Connect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
@@ -91,18 +72,14 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="data"></param>
|
||||
void Send(string data);
|
||||
/// <summary>
|
||||
/// Reset socket
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
void Reset();
|
||||
/// <returns></returns>
|
||||
Task ReconnectAsync();
|
||||
/// <summary>
|
||||
/// Close the connecting
|
||||
/// Close the connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task CloseAsync();
|
||||
/// <summary>
|
||||
/// Set proxy
|
||||
/// </summary>
|
||||
/// <param name="proxy"></param>
|
||||
void SetProxy(ApiProxy proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -11,18 +11,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="logger">The logger</param>
|
||||
/// <param name="parameters">The parameters to use for the connection</param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(Log log, string url);
|
||||
/// <summary>
|
||||
/// Create a websocket for an url
|
||||
/// </summary>
|
||||
/// <param name="log"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="cookies"></param>
|
||||
/// <param name="headers"></param>
|
||||
/// <returns></returns>
|
||||
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
|
||||
IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Log to console
|
||||
/// </summary>
|
||||
public class ConsoleLogger : ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Console.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Default log writer, writes to debug
|
||||
/// </summary>
|
||||
public class DebugLogger: ILogger
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Log implementation
|
||||
/// </summary>
|
||||
public class Log
|
||||
{
|
||||
/// <summary>
|
||||
/// List of ILogger implementations to forward the message to
|
||||
/// </summary>
|
||||
private List<ILogger> writers;
|
||||
|
||||
/// <summary>
|
||||
/// The verbosity of the logging, anything more verbose will not be forwarded to the writers
|
||||
/// </summary>
|
||||
public LogLevel? Level { get; set; } = LogLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="clientName">The name of the client the logging is used in</param>
|
||||
public Log(string clientName)
|
||||
{
|
||||
ClientName = clientName;
|
||||
writers = new List<ILogger>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the writers
|
||||
/// </summary>
|
||||
/// <param name="textWriters"></param>
|
||||
public void UpdateWriters(List<ILogger> textWriters)
|
||||
{
|
||||
writers = textWriters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a log entry
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The verbosity of the message</param>
|
||||
/// <param name="message">The message to log</param>
|
||||
public void Write(LogLevel logLevel, string message)
|
||||
{
|
||||
if (Level != null && (int)logLevel < (int)Level)
|
||||
return;
|
||||
|
||||
var logMessage = $"{ClientName,-10} | {message}";
|
||||
foreach (var writer in writers.ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.Log(logLevel, logMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Can't write to the logging so where else to output..
|
||||
Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.ToLogString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class AsyncResetEvent : IDisposable
|
||||
{
|
||||
private readonly static Task<bool> _completed = Task.FromResult(true);
|
||||
private readonly Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
private bool _signaled;
|
||||
private bool _reset;
|
||||
private readonly bool _reset;
|
||||
|
||||
/// <summary>
|
||||
/// New AsyncResetEvent
|
||||
@@ -50,7 +50,13 @@ namespace CryptoExchange.Net.Objects
|
||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
||||
var registration = cancellationSource.Token.Register(() =>
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
lock (_waits)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
|
||||
// Not the cleanest but it works
|
||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||
}
|
||||
}, useSynchronizationContext: false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
@@ -36,15 +39,11 @@ namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
return obj?.Success == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult CreateErrorResult(Error error)
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return new WebCallResult(null, null, error);
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,22 +61,36 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
public string? OriginalData { get; set; }
|
||||
public string? OriginalData { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="error"></param>
|
||||
#pragma warning disable 8618
|
||||
public CallResult([AllowNull]T data, Error? error): base(error)
|
||||
protected CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
|
||||
#pragma warning restore 8618
|
||||
{
|
||||
OriginalData = originalData;
|
||||
#pragma warning disable 8601
|
||||
Data = data;
|
||||
#pragma warning restore 8601
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new data result
|
||||
/// </summary>
|
||||
/// <param name="data">The data to return</param>
|
||||
public CallResult(T data) : this(data, null, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The erro rto return</param>
|
||||
public CallResult(Error error) : this(default, null, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
|
||||
/// </summary>
|
||||
@@ -111,16 +124,6 @@ namespace CryptoExchange.Net.Objects
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// </summary>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
public new static WebCallResult<T> CreateErrorResult(Error error)
|
||||
{
|
||||
return new WebCallResult<T>(null, null, default, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@@ -129,7 +132,42 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public CallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new CallResult<K>(data, Error);
|
||||
return new CallResult<K>(data, OriginalData, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDataless()
|
||||
{
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public CallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new CallResult(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error to return</param>
|
||||
/// <returns></returns>
|
||||
public CallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new CallResult<K>(default, OriginalData, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Success ? $"Success" : $"Error: {Error}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +176,26 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public class WebCallResult : CallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
@@ -148,40 +206,62 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code">Status code</param>
|
||||
/// <param name="responseHeaders">Response headers</param>
|
||||
/// <param name="error">Error</param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
/// <param name="requestHeaders"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error? error) : base(error)
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||
Error? error) : base(error)
|
||||
{
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code">Status code</param>
|
||||
/// <param name="responseHeaders">Response headers</param>
|
||||
/// <param name="error">Error</param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
|
||||
{
|
||||
return new WebCallResult(code, responseHeaders, error);
|
||||
}
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(Error error): base(error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// Return the result as an error result
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult CreateErrorResult(WebCallResult result)
|
||||
public WebCallResult AsError(Error error)
|
||||
{
|
||||
return new WebCallResult(result.ResponseStatusCode, result.ResponseHeaders, result.Error);
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,53 +271,107 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class WebCallResult<T>: CallResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The request http method
|
||||
/// </summary>
|
||||
public HttpMethod? RequestMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The headers sent with the request
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The url which was requested
|
||||
/// </summary>
|
||||
public string? RequestUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The body of the request
|
||||
/// </summary>
|
||||
public string? RequestBody { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||
/// </summary>
|
||||
public HttpStatusCode? ResponseStatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Length in bytes of the response
|
||||
/// </summary>
|
||||
public long? ResponseLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The response headers
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
[AllowNull] T data,
|
||||
Error? error): base(data, error)
|
||||
{
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// The time between sending the request and receiving the response
|
||||
/// </summary>
|
||||
public TimeSpan? ResponseTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new result
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="responseTime"></param>
|
||||
/// <param name="responseLength"></param>
|
||||
/// <param name="originalData"></param>
|
||||
/// <param name="requestUrl"></param>
|
||||
/// <param name="requestBody"></param>
|
||||
/// <param name="requestMethod"></param>
|
||||
/// <param name="requestHeaders"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="error"></param>
|
||||
public WebCallResult(
|
||||
HttpStatusCode? code,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
||||
TimeSpan? responseTime,
|
||||
long? responseLength,
|
||||
string? originalData,
|
||||
string? requestUrl,
|
||||
string? requestBody,
|
||||
HttpMethod? requestMethod,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||
[AllowNull] T data,
|
||||
Error? error) : base(data, error)
|
||||
Error? error) : base(data, originalData, error)
|
||||
{
|
||||
OriginalData = originalData;
|
||||
ResponseStatusCode = code;
|
||||
ResponseHeaders = responseHeaders;
|
||||
ResponseTime = responseTime;
|
||||
ResponseLength = responseLength;
|
||||
|
||||
RequestUrl = requestUrl;
|
||||
RequestBody = requestBody;
|
||||
RequestHeaders = requestHeaders;
|
||||
RequestMethod = requestMethod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDataless()
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
|
||||
}
|
||||
/// <summary>
|
||||
/// Copy as a dataless result
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public new WebCallResult AsDatalessError(Error error)
|
||||
{
|
||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new error result
|
||||
/// </summary>
|
||||
/// <param name="error">The error</param>
|
||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, default, error) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
@@ -246,19 +380,31 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <returns></returns>
|
||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||
{
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, OriginalData, data, Error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an error result
|
||||
/// Copy the WebCallResult to a new data type
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="responseHeaders"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <typeparam name="K">The new type</typeparam>
|
||||
/// <param name="error">The error returned</param>
|
||||
/// <returns></returns>
|
||||
public static WebCallResult<T> CreateErrorResult(HttpStatusCode? code, IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders, Error error)
|
||||
public new WebCallResult<K> AsError<K>(Error error)
|
||||
{
|
||||
return new WebCallResult<T>(code, responseHeaders, default, error);
|
||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(Success ? $"Success response" : $"Error response: {Error}");
|
||||
if (ResponseLength != null)
|
||||
sb.Append($", {ResponseLength} bytes");
|
||||
if (ResponseTime != null)
|
||||
sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,15 @@
|
||||
/// <summary>
|
||||
/// Data synced, order book is up to date
|
||||
/// </summary>
|
||||
Synced
|
||||
Synced,
|
||||
/// <summary>
|
||||
/// Disposing
|
||||
/// </summary>
|
||||
Disposing,
|
||||
/// <summary>
|
||||
/// Disposed
|
||||
/// </summary>
|
||||
Disposed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Base options
|
||||
/// </summary>
|
||||
public class BaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers.
|
||||
/// </summary>
|
||||
public LogLevel? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information;
|
||||
|
||||
/// <summary>
|
||||
/// The log writers
|
||||
/// </summary>
|
||||
public List<ILogger> LogWriters { get; set; } = new List<ILogger> { new DebugLogger() };
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, OutputOriginalData: {OutputOriginalData}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : BaseOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the order book implementation
|
||||
/// </summary>
|
||||
public string OrderBookName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.
|
||||
/// </summary>
|
||||
public bool SequenceNumbersAreConsecutive { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
|
||||
/// when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed
|
||||
/// </summary>
|
||||
public bool StrictLevels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the order book implementation</param>
|
||||
/// <param name="sequencesAreConsecutive">Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.</param>
|
||||
/// <param name="strictLevels">Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
|
||||
/// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed</param>
|
||||
public OrderBookOptions(string name, bool sequencesAreConsecutive, bool strictLevels)
|
||||
{
|
||||
OrderBookName = name;
|
||||
SequenceNumbersAreConsecutive = sequencesAreConsecutive;
|
||||
StrictLevels = strictLevels;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}, StrictLevels: {StrictLevels}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base client options
|
||||
/// </summary>
|
||||
public class ClientOptions : BaseOptions
|
||||
{
|
||||
private string _baseAddress;
|
||||
|
||||
/// <summary>
|
||||
/// The base address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress
|
||||
{
|
||||
get => _baseAddress;
|
||||
set
|
||||
{
|
||||
var newValue = value;
|
||||
if (!newValue.EndsWith("/"))
|
||||
newValue += "/";
|
||||
_baseAddress = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should check objects for missing properties based on the model and the received JSON
|
||||
/// </summary>
|
||||
public bool ShouldCheckObjects { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Proxy to use
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address to use</param>
|
||||
#pragma warning disable 8618
|
||||
public ClientOptions(string baseAddress)
|
||||
#pragma warning restore 8618
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for rest client options
|
||||
/// </summary>
|
||||
public class RestClientOptions : ClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when a call would exceed the rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// The time the server has to respond to a request before timing out
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored in requests and should be set on the provided HttpClient instance
|
||||
/// </summary>
|
||||
public HttpClient? HttpClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address of the API</param>
|
||||
public RestClientOptions(string baseAddress): base(baseAddress)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address of the API</param>
|
||||
/// <param name="httpClient">Shared http client instance</param>
|
||||
public RestClientOptions(HttpClient httpClient, string baseAddress) : base(baseAddress)
|
||||
{
|
||||
HttpClient = httpClient;
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a copy of the options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestClientOptions, new()
|
||||
{
|
||||
var copy = new T
|
||||
{
|
||||
BaseAddress = BaseAddress,
|
||||
LogLevel = LogLevel,
|
||||
Proxy = Proxy,
|
||||
LogWriters = LogWriters,
|
||||
RateLimiters = RateLimiters,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
RequestTimeout = RequestTimeout,
|
||||
HttpClient = HttpClient
|
||||
};
|
||||
|
||||
if (ApiCredentials != null)
|
||||
copy.ApiCredentials = ApiCredentials.Copy();
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base for socket client options
|
||||
/// </summary>
|
||||
public class SocketClientOptions : ClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of times to try to reconnect
|
||||
/// </summary>
|
||||
public int? MaxReconnectTries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of times to try to resubscribe after reconnecting
|
||||
/// </summary>
|
||||
public int? MaxResubscribeTries { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait for a socket response before giving a timeout
|
||||
/// </summary>
|
||||
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
/// <summary>
|
||||
/// The time after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected.
|
||||
/// </summary>
|
||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">The base address to use</param>
|
||||
public SocketClientOptions(string baseAddress) : base(baseAddress)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of the options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketClientOptions, new()
|
||||
{
|
||||
var copy = new T
|
||||
{
|
||||
BaseAddress = BaseAddress,
|
||||
LogLevel = LogLevel,
|
||||
Proxy = Proxy,
|
||||
LogWriters = LogWriters,
|
||||
AutoReconnect = AutoReconnect,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketResponseTimeout = SocketResponseTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget
|
||||
};
|
||||
|
||||
if (ApiCredentials != null)
|
||||
copy.ApiCredentials = ApiCredentials.Copy();
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for API usage
|
||||
/// </summary>
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool? OutputOriginalData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange options
|
||||
/// </summary>
|
||||
public class ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxy settings
|
||||
/// </summary>
|
||||
public ApiProxy? Proxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The max time a request is allowed to take
|
||||
/// </summary>
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(20);
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for order book options
|
||||
/// </summary>
|
||||
public class OrderBookOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||
/// </summary>
|
||||
public bool ChecksumValidationEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : OrderBookOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Http api options
|
||||
/// </summary>
|
||||
public class RestApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// List of rate limiters to use
|
||||
/// </summary>
|
||||
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||
|
||||
/// <summary>
|
||||
/// What to do when a call would exceed the rate limit
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool? AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan? TimestampRecalculationInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public virtual T Copy<T>() where T : RestApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
RateLimiters = RateLimiters,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Http API options
|
||||
/// </summary>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class RestApiOptions<TApiCredentials>: RestApiOptions where TApiCredentials: ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
public class RestExchangeOptions: ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment> : RestExchangeOptions where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a rest exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class RestExchangeOptions<TEnvironment, TApiCredentials> : RestExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Socket api options
|
||||
/// </summary>
|
||||
public class SocketApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan? SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Socket API options
|
||||
/// </summary>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class SocketApiOptions<TApiCredentials> : SocketApiOptions where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a websocket exchange client
|
||||
/// </summary>
|
||||
public class SocketExchangeOptions : ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the socket should automatically reconnect when losing connection
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Time to wait between reconnect attempts
|
||||
/// </summary>
|
||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||
/// </summary>
|
||||
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||
/// for example when the server sends intermittent ping requests
|
||||
/// </summary>
|
||||
public TimeSpan SocketNoDataTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
||||
/// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
||||
/// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
||||
/// </summary>
|
||||
public int? SocketSubscriptionsCombineTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
/// </summary>
|
||||
public TimeSpan DelayAfterConnect { get; set; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoReconnect = AutoReconnect,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment> : SocketExchangeOptions where TEnvironment : TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public TEnvironment Environment { get; set; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a socket exchange client
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
public class SocketExchangeOptions<TEnvironment, TApiCredentials> : SocketExchangeOptions<TEnvironment> where TEnvironment : TradeEnvironment where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests to this API.
|
||||
/// </summary>
|
||||
public new TApiCredentials? ApiCredentials
|
||||
{
|
||||
get => (TApiCredentials?)base.ApiCredentials;
|
||||
set => base.ApiCredentials = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Limits the amount of requests to a certain constraint
|
||||
/// </summary>
|
||||
public class RateLimiter : IRateLimiter
|
||||
{
|
||||
private readonly object _limiterLock = new object();
|
||||
internal List<Limiter> Limiters = new List<Limiter>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
||||
/// <see cref="AddEndpointLimit(string, int, TimeSpan, HttpMethod?, bool)"/>, <see cref="AddPartialEndpointLimit(string, int, TimeSpan, HttpMethod?, bool, bool)"/> or <see cref="AddApiKeyLimit"/>.
|
||||
/// </summary>
|
||||
public RateLimiter()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit for the total amount of requests per time period
|
||||
/// </summary>
|
||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint the limit is for</param>
|
||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
||||
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
|
||||
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The endpoints the limit is for</param>
|
||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
||||
/// <param name="excludeFromOtherRateLimits">If set to true it ignores other rate limits</param>
|
||||
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate lmit for the amount of requests per time for an endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint the limit is for</param>
|
||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||
/// <param name="method">The HttpMethod the limit is for, null for all</param>
|
||||
/// <param name="ignoreOtherRateLimits">If set to true it ignores other rate limits</param>
|
||||
/// <param name="countPerEndpoint">Whether all requests for this partial endpoint are bound to the same limit or each individual endpoint has its own limit</param>
|
||||
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit for the amount of requests per Api key
|
||||
/// </summary>
|
||||
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||
/// <param name="onlyForSignedRequests">Only include calls that are signed in this limiter</param>
|
||||
/// <param name="excludeFromTotalRateLimit">Exclude requests with API key from the total rate limiter</param>
|
||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
||||
{
|
||||
lock(_limiterLock)
|
||||
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult<int>> LimitRequestAsync(ILogger logger, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct)
|
||||
{
|
||||
int totalWaitTime = 0;
|
||||
|
||||
EndpointRateLimiter? endpointLimit;
|
||||
lock (_limiterLock)
|
||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||
if(endpointLimit != null)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
if (endpointLimit?.IgnoreOtherRateLimits == true)
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||
lock (_limiterLock)
|
||||
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
||||
foreach (var partialEndpointLimit in partialEndpointLimits)
|
||||
{
|
||||
if (partialEndpointLimit.CountPerEndpoint)
|
||||
{
|
||||
SingleTopicRateLimiter? thisEndpointLimit;
|
||||
lock (_limiterLock)
|
||||
{
|
||||
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
||||
if (thisEndpointLimit == null)
|
||||
{
|
||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
||||
Limiters.Add(thisEndpointLimit);
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(logger, thisEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, partialEndpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if(partialEndpointLimits.Any(p => p.IgnoreOtherRateLimits))
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
ApiKeyRateLimiter? apiLimit;
|
||||
lock (_limiterLock)
|
||||
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
||||
if (apiLimit != null)
|
||||
{
|
||||
if(apiKey == null)
|
||||
{
|
||||
if (!apiLimit.OnlyForSignedRequests)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, apiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
}
|
||||
else if (signed || !apiLimit.OnlyForSignedRequests)
|
||||
{
|
||||
SingleTopicRateLimiter? thisApiLimit;
|
||||
lock (_limiterLock)
|
||||
{
|
||||
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
||||
if (thisApiLimit == null)
|
||||
{
|
||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
||||
Limiters.Add(thisApiLimit);
|
||||
}
|
||||
}
|
||||
|
||||
var waitResult = await ProcessTopic(logger, thisApiLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if ((signed || apiLimit?.OnlyForSignedRequests == false) && apiLimit?.IgnoreTotalRateLimit == true)
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
|
||||
TotalRateLimiter? totalLimit;
|
||||
lock (_limiterLock)
|
||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||
if (totalLimit != null)
|
||||
{
|
||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||
if (!waitResult)
|
||||
return waitResult;
|
||||
|
||||
totalWaitTime += waitResult.Data;
|
||||
}
|
||||
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
|
||||
private static async Task<CallResult<int>> ProcessTopic(ILogger logger, Limiter historyTopic, string endpoint, int requestWeight, RateLimitingBehaviour limitBehaviour, CancellationToken ct)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await historyTopic.Semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
sw.Stop();
|
||||
|
||||
int totalWaitTime = 0;
|
||||
while (true)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||
{
|
||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||
{
|
||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||
i--;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
|
||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||
{
|
||||
if (currentWeight == 0)
|
||||
throw new Exception("Request limit reached without any prior request. " +
|
||||
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
||||
|
||||
// Wait until the next entry should be removed from the history
|
||||
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
|
||||
if (thisWaitTime > 0)
|
||||
{
|
||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
historyTopic.Semaphore.Release();
|
||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||
logger.Log(LogLevel.Warning, msg);
|
||||
return new CallResult<int>(new RateLimitError(msg));
|
||||
}
|
||||
|
||||
logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||
try
|
||||
{
|
||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new CallResult<int>(new CancellationRequestedError());
|
||||
}
|
||||
totalWaitTime += thisWaitTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var newTime = DateTime.UtcNow;
|
||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||
historyTopic.Semaphore.Release();
|
||||
return new CallResult<int>(totalWaitTime);
|
||||
}
|
||||
|
||||
internal struct LimitEntry
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public int Weight { get; set; }
|
||||
|
||||
public LimitEntry(DateTime timestamp, int weight)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
Weight = weight;
|
||||
}
|
||||
}
|
||||
|
||||
internal class Limiter
|
||||
{
|
||||
public RateLimitType Type { get; set; }
|
||||
public HttpMethod? Method { get; set; }
|
||||
|
||||
public SemaphoreSlim Semaphore { get; set; }
|
||||
public int Limit { get; set; }
|
||||
|
||||
public TimeSpan Period { get; set; }
|
||||
public List<LimitEntry> Entries { get; set; } = new List<LimitEntry>();
|
||||
|
||||
public Limiter(RateLimitType type, int limit, TimeSpan perPeriod, HttpMethod? method)
|
||||
{
|
||||
Semaphore = new SemaphoreSlim(1, 1);
|
||||
Type = type;
|
||||
Limit = limit;
|
||||
Period = perPeriod;
|
||||
Method = method;
|
||||
}
|
||||
}
|
||||
|
||||
internal class TotalRateLimiter : Limiter
|
||||
{
|
||||
public TotalRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method)
|
||||
: base(RateLimitType.Total, limit, perPeriod, method)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(TotalRateLimiter);
|
||||
}
|
||||
}
|
||||
|
||||
internal class EndpointRateLimiter: Limiter
|
||||
{
|
||||
public string[] Endpoints { get; set; }
|
||||
public bool IgnoreOtherRateLimits { get; set; }
|
||||
|
||||
public EndpointRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits)
|
||||
:base(RateLimitType.Endpoint, limit, perPeriod, method)
|
||||
{
|
||||
Endpoints = endpoints;
|
||||
IgnoreOtherRateLimits = ignoreOtherRateLimits;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(EndpointRateLimiter) + $": {string.Join(", ", Endpoints)}";
|
||||
}
|
||||
}
|
||||
|
||||
internal class PartialEndpointRateLimiter : Limiter
|
||||
{
|
||||
public string[] PartialEndpoints { get; set; }
|
||||
public bool IgnoreOtherRateLimits { get; set; }
|
||||
public bool CountPerEndpoint { get; set; }
|
||||
|
||||
public PartialEndpointRateLimiter(string[] partialEndpoints, int limit, TimeSpan perPeriod, HttpMethod? method, bool ignoreOtherRateLimits, bool countPerEndpoint)
|
||||
: base(RateLimitType.PartialEndpoint, limit, perPeriod, method)
|
||||
{
|
||||
PartialEndpoints = partialEndpoints;
|
||||
IgnoreOtherRateLimits = ignoreOtherRateLimits;
|
||||
CountPerEndpoint = countPerEndpoint;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return nameof(PartialEndpointRateLimiter) + $": {string.Join(", ", PartialEndpoints)}";
|
||||
}
|
||||
}
|
||||
|
||||
internal class ApiKeyRateLimiter : Limiter
|
||||
{
|
||||
public bool OnlyForSignedRequests { get; set; }
|
||||
public bool IgnoreTotalRateLimit { get; set; }
|
||||
|
||||
public ApiKeyRateLimiter(int limit, TimeSpan perPeriod, HttpMethod? method, bool onlyForSignedRequests, bool ignoreTotalRateLimit)
|
||||
:base(RateLimitType.ApiKey, limit, perPeriod, method)
|
||||
{
|
||||
OnlyForSignedRequests = onlyForSignedRequests;
|
||||
IgnoreTotalRateLimit = ignoreTotalRateLimit;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SingleTopicRateLimiter: Limiter
|
||||
{
|
||||
public object Topic { get; set; }
|
||||
|
||||
public SingleTopicRateLimiter(object topic, Limiter limiter)
|
||||
:base(limiter.Type, limiter.Limit, limiter.Period, limiter.Method)
|
||||
{
|
||||
Topic = topic;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (Type == RateLimitType.ApiKey ? nameof(ApiKeyRateLimiter): nameof(EndpointRateLimiter)) + $": {Topic}";
|
||||
}
|
||||
}
|
||||
|
||||
internal enum RateLimitType
|
||||
{
|
||||
Total,
|
||||
Endpoint,
|
||||
PartialEndpoint,
|
||||
ApiKey
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The time synchronization state of an API client
|
||||
/// </summary>
|
||||
public class TimeSyncState
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the API
|
||||
/// </summary>
|
||||
public string ApiName { get; set; }
|
||||
/// <summary>
|
||||
/// Semaphore to use for checking the time syncing. Should be shared instance among the API client
|
||||
/// </summary>
|
||||
public SemaphoreSlim Semaphore { get; }
|
||||
/// <summary>
|
||||
/// Last sync time for the API client
|
||||
/// </summary>
|
||||
public DateTime LastSyncTime { get; set; }
|
||||
/// <summary>
|
||||
/// Time offset for the API client
|
||||
/// </summary>
|
||||
public TimeSpan TimeOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TimeSyncState(string apiName)
|
||||
{
|
||||
ApiName = apiName;
|
||||
Semaphore = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time synchronization info
|
||||
/// </summary>
|
||||
public class TimeSyncInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
public ILogger Logger { get; }
|
||||
/// <summary>
|
||||
/// Should synchronize time
|
||||
/// </summary>
|
||||
public bool SyncTime { get; }
|
||||
/// <summary>
|
||||
/// Timestamp recalulcation interval
|
||||
/// </summary>
|
||||
public TimeSpan RecalculationInterval { get; }
|
||||
/// <summary>
|
||||
/// Time sync state for the API client
|
||||
/// </summary>
|
||||
public TimeSyncState TimeSyncState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger"></param>
|
||||
/// <param name="recalculationInterval"></param>
|
||||
/// <param name="syncTime"></param>
|
||||
/// <param name="syncState"></param>
|
||||
public TimeSyncInfo(ILogger logger, bool syncTime, TimeSpan recalculationInterval, TimeSyncState syncState)
|
||||
{
|
||||
Logger = logger;
|
||||
SyncTime = syncTime;
|
||||
RecalculationInterval = recalculationInterval;
|
||||
TimeSyncState = syncState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the time offset
|
||||
/// </summary>
|
||||
/// <param name="offset"></param>
|
||||
public void UpdateTimeOffset(TimeSpan offset)
|
||||
{
|
||||
TimeSyncState.LastSyncTime = DateTime.UtcNow;
|
||||
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500)
|
||||
{
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset within limits, set offset to 0ms");
|
||||
TimeSyncState.TimeOffset = TimeSpan.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log(LogLevel.Information, $"{TimeSyncState.ApiName} Time offset set to {Math.Round(offset.TotalMilliseconds)}ms");
|
||||
TimeSyncState.TimeOffset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trace logger provider for creating trace loggers
|
||||
/// </summary>
|
||||
public class TraceLoggerProvider : ILoggerProvider
|
||||
{
|
||||
private readonly LogLevel _logLevel;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logLevel"></param>
|
||||
public TraceLoggerProvider(LogLevel? logLevel = null)
|
||||
{
|
||||
_logLevel = logLevel ?? LogLevel.Trace;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trace logger
|
||||
/// </summary>
|
||||
public class TraceLogger : ILogger
|
||||
{
|
||||
private string? _categoryName;
|
||||
private LogLevel _logLevel;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="categoryName"></param>
|
||||
/// <param name="level"></param>
|
||||
public TraceLogger(string? categoryName = null, LogLevel level = LogLevel.Trace)
|
||||
{
|
||||
_categoryName = categoryName;
|
||||
_logLevel = level;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable BeginScope<TState>(TState state) => null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel;
|
||||
/// <inheritdoc />
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if ((int)logLevel < (int)_logLevel)
|
||||
return;
|
||||
|
||||
var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}";
|
||||
Trace.WriteLine(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Trade environment names
|
||||
/// </summary>
|
||||
public static class TradeEnvironmentNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Live environment
|
||||
/// </summary>
|
||||
public const string Live = "live";
|
||||
/// <summary>
|
||||
/// Testnet environment
|
||||
/// </summary>
|
||||
public const string Testnet = "testnet";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
||||
/// the echange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live`
|
||||
/// </summary>
|
||||
public class TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the environment
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
protected TradeEnvironment(string name)
|
||||
{
|
||||
EnvironmentName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user